fix(report): recommend by measured skin age and align admin results
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# 10.09 新版 H5 实施与验收
|
||||
|
||||
更新:2026-10-10。
|
||||
更新:2026-10-12。
|
||||
|
||||
## 按确认项实现
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
## 文档未定义部分的实施口径
|
||||
|
||||
话术文档没有新增计分公式或阈值,因此原六分类/三维度评分、肌龄计算及精华推荐规则不变。旧分档「预警/改善/良好」按低/中/高顺序映射为新文案的「改善/良好/优秀」。肤色和水分拆分展示,但继续共享原 `tone` 分类的分档,**不是新增独立评分算法**。
|
||||
话术文档没有新增计分公式或阈值,因此原六分类/三维度评分、肌龄计算不变。2026-10-12 经用户确认,精华推荐另按[肤龄基准与边界调整](产品推荐规则-肤龄版.md)实施,替代原三重判据;原始肌龄和展示肌龄在后台同时展示,历史快照不重算。旧分档「预警/改善/良好」按低/中/高顺序映射为新文案的「改善/良好/优秀」,后台同步此口径。肤色和水分拆分展示,但继续共享原 `tone` 分类的分档,**不是新增独立评分算法**。
|
||||
|
||||
痘痘话术按真实脓包/结节计数决定是否输出中重度皮损说明;没有计数不宣称「未检出」。当真实计数与高分模板冲突时,展示层选择可反映计数的文案,不改变原分数。
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
```bash
|
||||
node h5/js/skin-score.js
|
||||
node h5/test/regression.js
|
||||
node h5/test/recommendation.js
|
||||
node h5/test/accounts.js
|
||||
node h5/test/report-persistence.mjs
|
||||
node h5/test/wechat-browser.mjs
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
# 产品推荐规则:肤龄基准与边界调整
|
||||
|
||||
状态:2026-10-12 用户已确认三个边界条件;已实现。规则版本:`2026-10-12-skin-age-v1`。
|
||||
|
||||
## 已明确的要求
|
||||
|
||||
- 基准只取 Face++ 返回的原始 `skin_age`,不取实际年龄或修正后的展示肤龄。
|
||||
- 基础分组:肤龄小于 35 岁为 25+(主场精华 I),35 至不足 50 岁为 35+(II),50 岁及以上为 50+(III)。24 岁等不足 25 岁的检测结果也归 I。
|
||||
- 肤龄小于 32 岁,固定推荐 I,即使轮廓或皱纹得分低,也不触发升级。
|
||||
- 删除原来的实际年龄组、六分类均分、最差分类三重判据。
|
||||
- 边界附近结合三维度评分调整:紧塑力为必要条件,另外一个达标维度可为透亮感或稳韧力;不能仅因为另外两个维度高而降档。
|
||||
- 37 岁肌龄、仅存在暗沉问题的案例,应允许推荐 I。
|
||||
- 每份新报告仍只推荐一款;评分公式、检测流程和已有报告快照不因本次规则调整而改写。
|
||||
- 原始肤龄缺失或无效时,不回退实际年龄推测产品;需要明确显示本次无法推荐。
|
||||
|
||||
## 已确认的三个条件
|
||||
|
||||
1. 只在 35±3 岁及 50±3 岁范围内调整,范围外只按肤龄。
|
||||
2. 50 岁边界与 35 岁同样处理:47~不足50 岁紧塑力低于 8 分升至 III,50~53 岁紧塑力至少 9 分且另一维度至少 9 分降至 II。
|
||||
3. 「7+最低档」为低于 8 分;「9+」为大于等于 9 分,恰好 8 分不升档,恰好 9 分可以满足降档条件。
|
||||
|
||||
## 决策顺序
|
||||
|
||||
年龄支持小数,区间严格按下表执行:
|
||||
|
||||
| 原始肤龄 | 默认产品 | 可调整条件 |
|
||||
|---|---|---|
|
||||
| 小于 32 | I | 固定,不升档 |
|
||||
| 32~不足35 | I | 紧塑力低于8,升为II |
|
||||
| 35~38(含38) | II | 紧塑力至少9,且透亮感/稳韧力任一至少9,降为I |
|
||||
| 大于38~不足47 | II | 按肤龄,不调整 |
|
||||
| 47~不足50 | II | 紧塑力低于8,升为III |
|
||||
| 50~53(含53) | III | 紧塑力至少9,且透亮感/稳韧力任一至少9,降为II |
|
||||
| 大于53 | III | 按肤龄,不调整 |
|
||||
|
||||
调整最多一档。没有足够的必要维度数据证明满足调整条件时,保持肤龄基础档;不把缺失值当成低分。
|
||||
|
||||
比较分数沿用评分引擎的三维度原始 `S=7+3×P/100`,不先进行前端一位小数或 VM 两位小数舍入。例如 7.99999 小于 8,8.99999 小于 9。原六分类和三维度评分公式、低/中/高档位阈值均不变;推荐阈值与档位文案阈值是不同概念。
|
||||
|
||||
## 后台与前端一致性
|
||||
|
||||
- 后台列表新增「肌龄(原始 / 展示)」;档案里的年龄明确为实际年龄。
|
||||
- 名称统一为紧塑力、透亮感、稳韧力;列表显示与 H5 相同的一位小数分数。内部旧档位 `预警/改善/良好` 映射成 `改善/良好/优秀`,筛选参数仍传原值,避免改写旧数据或破坏统计。
|
||||
- 新记录同时保存 `skin_age_display`、`recommendation` 和前端 `presentation`。后台详情展示基础档、最终档、未舍入的判定分、推荐原因与规则版本;七项结论和护理建议直接使用同一份前端快照。
|
||||
- 旧记录没有新规则元数据时,明确显示「旧版规则(历史记录)」,不按新规则重新推荐。后台详情可读取原账户已保存的前端展示快照;无快照则标注旧版文案。旧记录未存展示肌龄时,按原有实际年龄±3规则只读补全展示值。
|
||||
- CSV 同步提供两种肌龄、统一的新档位名、推荐版本和原因。
|
||||
|
||||
## 实施与验证范围
|
||||
|
||||
- `h5/js/skin-score.js`:推荐决策、产品组别、推荐原因及相关综合文案;保留原评分算法。
|
||||
- `h5/js/report-ui.js`:前后台共用名称、档位、三款产品映射;H5 无有效肤龄时隐藏具体产品并说明原因。
|
||||
- 更新引擎自测和 Excel 回归中的产品相关断言;原始 Excel 保留为旧算法参照,不改写评分真值来掩盖差异。
|
||||
- 新增边界测试:24岁且轮廓/皱纹低分;31.99/32/34.99/35/38/38.01;46.99/47/49.99/50/53/53.01;紧塑力7.99/8/8.99/9;另两个维度分别达标;必要条件不满足;实际年龄改变不影响推荐;原始肤龄和显示肤龄不同;维度或肤龄缺失。
|
||||
- 验证新报告保存与重开后产品一致,已有报告仍读取原有快照。
|
||||
- 本次不调整展示肤龄的实际年龄±3岁修正规则,后台并列展示两者,推荐只使用原始肤龄。
|
||||
|
||||
验证命令:
|
||||
|
||||
```bash
|
||||
node h5/test/recommendation.js
|
||||
node h5/js/skin-score.js
|
||||
node h5/test/regression.js
|
||||
node h5/test/accounts.js
|
||||
node h5/test/report-persistence.mjs
|
||||
```
|
||||
|
||||
已通过 126 个推荐用例、83 条评分引擎自测、86 条真值回归。隔离 HTTP + Chromium 验证前后台原始/展示肌龄、三维度评分、推荐产品、七项结论、护理建议、筛选与 CSV 一致;验证保存重开、旧报告快照不改写、无肤龄时不残留上一款产品。全部使用样本上游,不调用收费 Face++。
|
||||
@@ -3,31 +3,31 @@
|
||||
"1": "线上闭环测试(不含真实微信授权)",
|
||||
"2": "app.js",
|
||||
"3": "skin-score.js",
|
||||
"4": "n",
|
||||
"5": "normalize",
|
||||
"6": "getContext2dOrThrow",
|
||||
"7": "extractFaces",
|
||||
"4": "i",
|
||||
"5": "n",
|
||||
"6": ".runAndExtendWithFaceDetections",
|
||||
"7": "Box",
|
||||
"8": "admin.js",
|
||||
"9": "__awaiter",
|
||||
"10": "AgeGenderNet",
|
||||
"11": "lib/accounts.js",
|
||||
"12": "server.js",
|
||||
"13": "files",
|
||||
"14": "FaceLandmarks",
|
||||
"14": "FaceLandmarks68",
|
||||
"15": ".runAndExtendWithFaceDetection",
|
||||
"16": "巨子测肤 · 1'me 在线肌肤测试 — UI 设计规范",
|
||||
"17": "getEnv",
|
||||
"17": "extractFaces",
|
||||
"18": "1'me 在线肌肤测试 H5",
|
||||
"19": "head-pose.js",
|
||||
"20": "build_fixtures.py",
|
||||
"21": "disposeUnusedWeightTensors",
|
||||
"22": ".computeFaceDescriptor",
|
||||
"22": "ComputeAllFaceDescriptorsTask",
|
||||
"23": "facepp-skinanalyze(旷视皮肤分析专业版调用技能)",
|
||||
"24": "巨子测肤 · 1'me 语音播报内容设计文档 V1.0",
|
||||
"25": "mobile-select.iife.min.js",
|
||||
"26": "wi",
|
||||
"27": "gen_voice.py",
|
||||
"28": "wn",
|
||||
"28": "C",
|
||||
"29": "face-api.js 本地化依赖(h5/vendor/faceapi/)",
|
||||
"30": "ui-20261009/manifest.json",
|
||||
"31": "browser-failures.mjs",
|
||||
@@ -35,49 +35,51 @@
|
||||
"33": "build-fixture.py",
|
||||
"34": "smoke-test.js",
|
||||
"35": "DetectSingleFaceLandmarksTask",
|
||||
"36": "so",
|
||||
"36": "ao",
|
||||
"37": "UI 外发版 10.09 图片资源",
|
||||
"38": "PredictSingleAgeAndGenderWithFaceAlignmentTask",
|
||||
"38": "FaceLandmark68Net",
|
||||
"39": "UI交互界面调整",
|
||||
"40": "extractAllFacesAndComputeResults",
|
||||
"41": "initialize",
|
||||
"40": ".predictAgeAndGender",
|
||||
"41": "test/accounts.js",
|
||||
"42": "export_psd_assets.py",
|
||||
"43": "FaceLandmark68Net",
|
||||
"43": ".loadFromDisk",
|
||||
"44": "start-local.sh",
|
||||
"45": "PredictAllAgeAndGenderWithFaceAlignmentTask",
|
||||
"46": "PredictAllFaceExpressionsWithFaceAlignmentTask",
|
||||
"47": "TinyYolov2Base",
|
||||
"48": "PredictSingleFaceExpressionsWithFaceAlignmentTask",
|
||||
"47": "recommendation.js",
|
||||
"48": "report-persistence.mjs",
|
||||
"49": "mobile-select/README.md",
|
||||
"50": "call_skinanalyze_3view.sh",
|
||||
"51": "第二轮2反馈.md",
|
||||
"52": "region-data.js",
|
||||
"53": "护理建议(全量文本,按需求摘抄)",
|
||||
"54": "UI 对齐 10.09 设计稿:调研与实施计划",
|
||||
"55": "NeuralNetwork",
|
||||
"56": "convLayer",
|
||||
"55": "extractFaceTensors",
|
||||
"56": "report-ui.js",
|
||||
"57": "regression.js",
|
||||
"58": ".dispose",
|
||||
"59": "call_skinanalyze.sh",
|
||||
"60": "report-fixture.js",
|
||||
"61": ".extractWeights",
|
||||
"61": "NeuralNetwork",
|
||||
"62": "提交前 Review(2026-10-11)",
|
||||
"63": "e2e-browser.mjs",
|
||||
"64": "account-client.js",
|
||||
"65": "tt",
|
||||
"66": "cn",
|
||||
"65": "k",
|
||||
"66": "wechat-browser.mjs",
|
||||
"67": "ui-20261009.mjs",
|
||||
"68": "analyze-timeout.js",
|
||||
"69": "misans/README.md",
|
||||
"70": "decodeBoxesLayer",
|
||||
"71": "dr",
|
||||
"70": ".draw",
|
||||
"71": "production-loop.mjs",
|
||||
"72": "build_report_copy.py",
|
||||
"73": "camera-geometry.js",
|
||||
"74": "report-copy.js",
|
||||
"75": "prepare_h5_assets.py",
|
||||
"76": "TinyYolov2",
|
||||
"77": "FaceDetection",
|
||||
"77": "drawFaceExpressions",
|
||||
"78": ".toString",
|
||||
"79": "isValidNumber",
|
||||
"80": "线上微信登录部署记录",
|
||||
"83": "Yo"
|
||||
"81": "ComputeSingleFaceDescriptorTask",
|
||||
"82": "PredictAllFaceExpressionsTask"
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"0": "69ee47981e97a7eb", "1": "cb6ea50bd4e4a5ca", "2": "9cf893b45c314643", "3": "8ac814cdb72a1a7d", "4": "0f24ec9aab01e512", "5": "0e8200cefc11b932", "6": "1169f97bb458af6f", "7": "c1ae3952d970e510", "8": "cf2aba493368e557", "9": "8abf7998360e0ccc", "10": "c796af251148a471", "11": "79c3e26b93d030ce", "12": "ab705784bbd1dbdc", "13": "7e3b558a587a4ebe", "14": "186e1f68d4b2dfe9", "15": "5a9014c4a7f87699", "16": "95add6a975c0cb72", "17": "773ca6b3d5102a78", "18": "5aa9c1fc3b3c8196", "19": "0a373ad2197fd1a6", "20": "aecf2fce44ba49c5", "21": "5d94cf57d5d1a6fb", "22": "444ef0744645f5ac", "23": "90fac00055070ce2", "24": "f85b5b1634224c86", "25": "46e8db3e6e3f37ea", "26": "83b0fd3b14363e4d", "27": "eab427ce9a5219e9", "28": "cf86030ab36386aa", "29": "c608f1c6492e1cb1", "30": "73c13793fca09f56", "31": "fbbcfc059751cc6a", "32": "f6bcf56c4589316b", "33": "75f750c5b7d9f63d", "34": "f024ff7942935690", "35": "23f1d406f6c779ae", "36": "c55f872b3e16ab6f", "37": "cb8f26249485324a", "38": "01f68dfc34a29d32", "39": "fdd95375b4a5ddb3", "40": "3a68564642f68c0d", "41": "eb3733e409dc7411", "42": "ffd960136fde00ad", "43": "9a25b1230b5fe649", "44": "852c1bb2446edd9c", "45": "92ab3e5134ed8685", "46": "65718282b3c32109", "47": "41f3bef447e57f23", "48": "8c471c7baeaaca54", "49": "1f976711cda158dc", "50": "bbb2b57d72922b16", "51": "eb6a182204867275", "52": "2ec3c8c088568ba9", "53": "68c45056378341a7", "54": "c58d41e8e580fdc6", "55": "d96493a5f110bd4c", "56": "e649ddeadda89cbb", "57": "f2a40b5f1b29f669", "58": "73a31170f76a338b", "59": "891f59cda3708718", "60": "a505b7de6257a4e8", "61": "400fa8ac8b726a66", "62": "1366ae24c7e5897d", "63": "c256b43f75d7809e", "64": "86101547c05f96b9", "65": "3088ae48043ee39d", "66": "932f010dc6115fb4", "67": "3e126d8deaf6e200", "68": "6b91f3679541de79", "69": "e511846eeba03076", "70": "c396c038e5f5007f", "71": "2bfc96d4090afd84", "72": "c84f196a7bd35861", "73": "2b887bf601ccfb9f", "74": "ae2a154311e1f53a", "75": "69fdd23278518884", "76": "29790cf710874dd0", "77": "287d60115d9acda8", "79": "8f647d3ce08ecd2b", "80": "059d6ba8ca7ec3a0", "83": "a54008a71fdd44fa"}
|
||||
{"0": "73743c18a4a7fca9", "1": "cb6ea50bd4e4a5ca", "2": "9cf893b45c314643", "3": "1f737ca8edb978f2", "4": "55697fad20675b78", "5": "be544c1e1f2ddb64", "6": "46204fb2e4d50c53", "7": "fcfa9f1898866dc8", "8": "1ecf47531e1d507b", "9": "2e6b02a61343f092", "10": "563202faef7b3421", "11": "edc9f95405704875", "12": "d3e946c5907aa577", "13": "7e3b558a587a4ebe", "14": "15606ce5d8eec23a", "15": "5a9014c4a7f87699", "16": "95add6a975c0cb72", "17": "2405e1edb003a83e", "18": "300b45cf926eb704", "19": "0a373ad2197fd1a6", "20": "aecf2fce44ba49c5", "21": "d3cd661c24264482", "22": "3dd68cce72af5314", "23": "90fac00055070ce2", "24": "f85b5b1634224c86", "25": "46e8db3e6e3f37ea", "26": "6b6c40a418e18aed", "27": "eab427ce9a5219e9", "28": "00f6d1f8841af9c4", "29": "c608f1c6492e1cb1", "30": "73c13793fca09f56", "31": "fbbcfc059751cc6a", "32": "f6bcf56c4589316b", "33": "75f750c5b7d9f63d", "34": "f024ff7942935690", "35": "23f1d406f6c779ae", "36": "8e5e97153c881130", "37": "cb8f26249485324a", "38": "e2d12f8289f83819", "39": "fdd95375b4a5ddb3", "40": "a2b13160223b5a98", "41": "4bc370be8afd7db1", "42": "ffd960136fde00ad", "43": "00dab8704da757dc", "44": "852c1bb2446edd9c", "45": "92ab3e5134ed8685", "46": "65718282b3c32109", "47": "e594ecaa8e7d1538", "48": "e042d7fe7351bcb9", "49": "1f976711cda158dc", "50": "bbb2b57d72922b16", "51": "eb6a182204867275", "52": "2ec3c8c088568ba9", "53": "68c45056378341a7", "54": "c58d41e8e580fdc6", "55": "0d376c2f52a725e0", "56": "91d7ab49459e0b92", "57": "f2a40b5f1b29f669", "58": "675690fb61ef5bc7", "59": "891f59cda3708718", "60": "a505b7de6257a4e8", "61": "18ffa2c1feddfd10", "62": "1366ae24c7e5897d", "63": "c256b43f75d7809e", "64": "86101547c05f96b9", "65": "b7874aef5ca3f2ee", "66": "2b44d0efd8eb25c5", "67": "3e126d8deaf6e200", "68": "6b91f3679541de79", "69": "e511846eeba03076", "70": "bf75bfc99d4f9e34", "71": "4195478d5c528371", "72": "c84f196a7bd35861", "73": "2b887bf601ccfb9f", "74": "ae2a154311e1f53a", "75": "69fdd23278518884", "76": "29790cf710874dd0", "77": "a0bc88eb14b70170", "78": "42c9bc548e269a96", "79": "1c27e5f448dc4f9c", "80": "059d6ba8ca7ec3a0", "81": "7ac475ea9bc280f8", "82": "e800d832c255899a"}
|
||||
+127
-113
@@ -1,16 +1,16 @@
|
||||
# Graph Report - juzi (2026-10-11)
|
||||
# Graph Report - juzi (2026-10-12)
|
||||
|
||||
## Corpus Check
|
||||
- 64 files · ~4,182,459 words
|
||||
- 66 files · ~4,184,136 words
|
||||
- Verdict: corpus is large enough that graph structure adds value.
|
||||
|
||||
## Summary
|
||||
- 1164 nodes · 2523 edges · 81 communities (63 shown, 18 thin omitted)
|
||||
- 1188 nodes · 2566 edges · 83 communities (66 shown, 17 thin omitted)
|
||||
- Extraction: 93% EXTRACTED · 7% INFERRED · 0% AMBIGUOUS · INFERRED: 179 edges (avg confidence: 0.53)
|
||||
- Token cost: 0 input · 0 output
|
||||
|
||||
## Graph Freshness
|
||||
- Built from commit: `3dda4ec7`
|
||||
- Built from commit: `7518dc23`
|
||||
- Run `git rev-parse HEAD` and compare to check if the graph is stale.
|
||||
- Run `graphify update .` after code changes (no API cost).
|
||||
|
||||
@@ -19,76 +19,78 @@
|
||||
- 线上闭环测试(不含真实微信授权)
|
||||
- app.js
|
||||
- skin-score.js
|
||||
- i
|
||||
- n
|
||||
- normalize
|
||||
- getContext2dOrThrow
|
||||
- extractFaces
|
||||
- .runAndExtendWithFaceDetections
|
||||
- Box
|
||||
- admin.js
|
||||
- __awaiter
|
||||
- AgeGenderNet
|
||||
- lib/accounts.js
|
||||
- server.js
|
||||
- files
|
||||
- FaceLandmarks
|
||||
- FaceLandmarks68
|
||||
- .runAndExtendWithFaceDetection
|
||||
- 巨子测肤 · 1'me 在线肌肤测试 — UI 设计规范
|
||||
- getEnv
|
||||
- extractFaces
|
||||
- 1'me 在线肌肤测试 H5
|
||||
- head-pose.js
|
||||
- build_fixtures.py
|
||||
- disposeUnusedWeightTensors
|
||||
- .computeFaceDescriptor
|
||||
- ComputeAllFaceDescriptorsTask
|
||||
- facepp-skinanalyze(旷视皮肤分析专业版调用技能)
|
||||
- 巨子测肤 · 1'me 语音播报内容设计文档 V1.0
|
||||
- mobile-select.iife.min.js
|
||||
- wi
|
||||
- gen_voice.py
|
||||
- wn
|
||||
- C
|
||||
- face-api.js 本地化依赖(h5/vendor/faceapi/)
|
||||
- ui-20261009/manifest.json
|
||||
- browser-failures.mjs
|
||||
- FaceMatcher
|
||||
- build-fixture.py
|
||||
- DetectSingleFaceLandmarksTask
|
||||
- so
|
||||
- ao
|
||||
- UI 外发版 10.09 图片资源
|
||||
- PredictSingleAgeAndGenderWithFaceAlignmentTask
|
||||
- UI交互界面调整
|
||||
- extractAllFacesAndComputeResults
|
||||
- initialize
|
||||
- export_psd_assets.py
|
||||
- FaceLandmark68Net
|
||||
- UI交互界面调整
|
||||
- .predictAgeAndGender
|
||||
- test/accounts.js
|
||||
- export_psd_assets.py
|
||||
- .loadFromDisk
|
||||
- start-local.sh
|
||||
- PredictAllAgeAndGenderWithFaceAlignmentTask
|
||||
- PredictAllFaceExpressionsWithFaceAlignmentTask
|
||||
- TinyYolov2Base
|
||||
- PredictSingleFaceExpressionsWithFaceAlignmentTask
|
||||
- recommendation.js
|
||||
- report-persistence.mjs
|
||||
- mobile-select/README.md
|
||||
- call_skinanalyze_3view.sh
|
||||
- 护理建议(全量文本,按需求摘抄)
|
||||
- UI 对齐 10.09 设计稿:调研与实施计划
|
||||
- NeuralNetwork
|
||||
- convLayer
|
||||
- extractFaceTensors
|
||||
- report-ui.js
|
||||
- regression.js
|
||||
- .dispose
|
||||
- call_skinanalyze.sh
|
||||
- .extractWeights
|
||||
- NeuralNetwork
|
||||
- 提交前 Review(2026-10-11)
|
||||
- e2e-browser.mjs
|
||||
- account-client.js
|
||||
- tt
|
||||
- cn
|
||||
- k
|
||||
- wechat-browser.mjs
|
||||
- ui-20261009.mjs
|
||||
- analyze-timeout.js
|
||||
- misans/README.md
|
||||
- decodeBoxesLayer
|
||||
- dr
|
||||
- .draw
|
||||
- production-loop.mjs
|
||||
- build_report_copy.py
|
||||
- TinyYolov2
|
||||
- FaceDetection
|
||||
- drawFaceExpressions
|
||||
- .toString
|
||||
- isValidNumber
|
||||
- 线上微信登录部署记录
|
||||
- Yo
|
||||
- ComputeSingleFaceDescriptorTask
|
||||
- PredictAllFaceExpressionsTask
|
||||
|
||||
## God Nodes (most connected - your core abstractions)
|
||||
1. `__awaiter()` - 59 edges
|
||||
@@ -100,7 +102,7 @@
|
||||
7. `C()` - 32 edges
|
||||
8. `toNetInput()` - 27 edges
|
||||
9. `e()` - 22 edges
|
||||
10. `f()` - 20 edges
|
||||
10. `loadRecords()` - 20 edges
|
||||
|
||||
## Surprising Connections (you probably didn't know these)
|
||||
- `loadRecords()` --indirect_call--> `it()` [INFERRED]
|
||||
@@ -117,11 +119,11 @@
|
||||
## Import Cycles
|
||||
- None detected.
|
||||
|
||||
## Communities (81 total, 18 thin omitted)
|
||||
## Communities (83 total, 17 thin omitted)
|
||||
|
||||
### Community 0 - "face-api.js"
|
||||
Cohesion: 0.02
|
||||
Nodes (57): bn(), Bo(), ca(), cd(), ce(), computeReshapedDimensions(), dn(), ee() (+49 more)
|
||||
Nodes (35): cn(), computeReshapedDimensions(), createBrowserEnv(), createFileSystem(), createNodejsEnv(), dr(), en(), FaceDetection() (+27 more)
|
||||
|
||||
### Community 1 - "线上闭环测试(不含真实微信授权)"
|
||||
Cohesion: 0.25
|
||||
@@ -133,51 +135,47 @@ Nodes (74): applyMirror(), armNudge(), beginDetect(), bindBackdropClose(), busyW
|
||||
|
||||
### Community 3 - "skin-score.js"
|
||||
Cohesion: 0.12
|
||||
Nodes (44): acneGrade(), buildViewModel(), calcAcne(), calcContour(), calcSens(), calcSmooth(), calcTone(), calcWrinkles() (+36 more)
|
||||
Nodes (45): acneGrade(), buildViewModel(), calcAcne(), calcContour(), calcSens(), calcSmooth(), calcTone(), calcWrinkles() (+37 more)
|
||||
|
||||
### Community 4 - "n"
|
||||
Cohesion: 0.07
|
||||
Nodes (88): a(), ae(), ao(), b(), ba(), be(), bf(), bi() (+80 more)
|
||||
### Community 4 - "i"
|
||||
Cohesion: 0.09
|
||||
Nodes (42): a(), bf(), cf(), d(), da(), dn(), DrawBox(), drawContour() (+34 more)
|
||||
|
||||
### Community 5 - "normalize"
|
||||
### Community 5 - "n"
|
||||
Cohesion: 0.11
|
||||
Nodes (17): conv(), conv$1(), convDown(), convLayer$1(), convNoRelu(), denseBlock3(), denseBlock4(), depthwiseSeparableConv() (+9 more)
|
||||
Nodes (37): ae(), b(), be(), bi(), ce(), de(), e(), Ef() (+29 more)
|
||||
|
||||
### Community 6 - "getContext2dOrThrow"
|
||||
Cohesion: 0.12
|
||||
Nodes (15): DetectAllFacesTask(), DrawBox(), drawDetections(), drawFaceExpressions(), DrawFaceLandmarks(), DrawTextField(), extendWithFaceDetection(), FaceExpressions() (+7 more)
|
||||
|
||||
### Community 7 - "extractFaces"
|
||||
Cohesion: 0.15
|
||||
Nodes (5): Box(), DetectAllFaceLandmarksTask(), extractFaces(), extractFaceTensors(), isTensor3D()
|
||||
### Community 7 - "Box"
|
||||
Cohesion: 0.10
|
||||
Nodes (11): Box(), extendWithFaceDescriptor(), extendWithFaceDetection(), extendWithFaceLandmarks(), FaceLandmarks(), FaceRecognitionNet(), isWithFaceDetection(), isWithFaceLandmarks() (+3 more)
|
||||
|
||||
### Community 8 - "admin.js"
|
||||
Cohesion: 0.25
|
||||
Nodes (23): api(), bind(), closeDetail(), collectFilters(), dimsHtml(), doRetry(), dSec(), el() (+15 more)
|
||||
Cohesion: 0.21
|
||||
Nodes (28): ageText(), api(), bind(), closeDetail(), collectFilters(), dimsHtml(), doRetry(), dSec() (+20 more)
|
||||
|
||||
### Community 9 - "__awaiter"
|
||||
Cohesion: 0.14
|
||||
Nodes (12): __awaiter(), ComposableTask(), fetchImage(), fetchJson(), fetchNetWeights(), fetchOrThrow(), __generator(), getModelUris() (+4 more)
|
||||
Nodes (13): __awaiter(), extendWithFaceExpressions(), extractAllFacesAndComputeResults(), extractSingleFaceAndComputeResult(), fetchImage(), fetchJson(), fetchNetWeights(), fetchOrThrow() (+5 more)
|
||||
|
||||
### Community 10 - "AgeGenderNet"
|
||||
Cohesion: 0.11
|
||||
Nodes (5): AgeGenderNet(), FaceExpressionNet(), FaceLandmark68NetBase(), FaceLandmark68TinyNet(), FaceProcessor()
|
||||
Cohesion: 0.05
|
||||
Nodes (31): AgeGenderNet(), conv(), conv$1(), convDown(), convLayer(), convLayer$1(), convNoRelu(), convWithBatchNorm() (+23 more)
|
||||
|
||||
### Community 11 - "lib/accounts.js"
|
||||
Cohesion: 0.06
|
||||
Nodes (43): build(), count(), fill(), numeric(), atomic(), body(), cookie(), cookies() (+35 more)
|
||||
Cohesion: 0.17
|
||||
Nodes (19): atomic(), body(), cookie(), cookies(), crypto, fs, json(), login() (+11 more)
|
||||
|
||||
### Community 12 - "server.js"
|
||||
Cohesion: 0.08
|
||||
Nodes (48): accounts, appendRecord(), callFacepp(), CRED, csvCell(), DATA_DIR, dumpRaw(), ERR_MSG (+40 more)
|
||||
Nodes (49): accounts, appendRecord(), callFacepp(), CRED, csvCell(), DATA_DIR, displaySkinAge(), dumpRaw() (+41 more)
|
||||
|
||||
### Community 13 - "files"
|
||||
Cohesion: 0.12
|
||||
Nodes (15): files, analyze-fail, analyzing, countdown-321, hint-far, hint-front, hint-hold, hint-near (+7 more)
|
||||
|
||||
### Community 14 - "FaceLandmarks"
|
||||
Cohesion: 0.10
|
||||
Nodes (9): bgrToRgbTensor(), extendWithFaceLandmarks(), FaceLandmarks(), FaceLandmarks5(), FaceLandmarks68(), getCenterPoint(), isDimensions(), minBbox() (+1 more)
|
||||
### Community 14 - "FaceLandmarks68"
|
||||
Cohesion: 0.19
|
||||
Nodes (3): FaceLandmarks5(), FaceLandmarks68(), getCenterPoint()
|
||||
|
||||
### Community 15 - ".runAndExtendWithFaceDetection"
|
||||
Cohesion: 0.33
|
||||
@@ -187,13 +185,13 @@ Nodes (5): allFacesMtcnn(), allFacesSsdMobilenetv1(), allFacesTinyYolov2(), dete
|
||||
Cohesion: 0.15
|
||||
Nodes (12): 1. 基准与尺寸换算, 2. 色彩规范, 3. 字体规范, 4. 组件切图(assets/svg/), 5. 图片素材(assets/images/,共 15 张), 6. 页面清单(design/ 下有整屏 PNG 预览), 7. 目录结构, 8. 还原建议 (+4 more)
|
||||
|
||||
### Community 17 - "getEnv"
|
||||
### Community 17 - "extractFaces"
|
||||
Cohesion: 0.19
|
||||
Nodes (14): awaitMediaLoaded(), bufferToImage(), createCanvas(), createCanvasFromMedia(), extractImagePatches(), getEnv(), getMediaDimensions(), imageTensorToCanvas() (+6 more)
|
||||
Nodes (19): awaitMediaLoaded(), bufferToImage(), createCanvas(), createCanvasFromMedia(), extractFaces(), extractImagePatches(), getContext2dOrThrow(), getEnv() (+11 more)
|
||||
|
||||
### Community 18 - "1'me 在线肌肤测试 H5"
|
||||
Cohesion: 0.07
|
||||
Nodes (27): 10.09 新版 H5 实施与验收, 按确认项实现, 文档未定义部分的实施口径, 素材再生成, 验证命令, 公网服务器配置, 公网真机验收清单, 反向代理与后台保护 (+19 more)
|
||||
Cohesion: 0.05
|
||||
Nodes (33): 10.09 新版 H5 实施与验收, 按确认项实现, 文档未定义部分的实施口径, 素材再生成, 验证命令, 产品推荐规则:肤龄基准与边界调整, 决策顺序, 后台与前端一致性 (+25 more)
|
||||
|
||||
### Community 19 - "head-pose.js"
|
||||
Cohesion: 0.33
|
||||
@@ -204,12 +202,8 @@ 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.09
|
||||
Nodes (23): disposeUnusedWeightTensors(), extractorsFactory$3(), extractorsFactory$5(), extractorsFactory$7(), extractorsFactory$9(), extractParamsFromWeigthMap(), extractParamsFromWeigthMap$1(), extractParamsFromWeigthMap$2() (+15 more)
|
||||
|
||||
### Community 22 - ".computeFaceDescriptor"
|
||||
Cohesion: 0.18
|
||||
Nodes (4): ComputeAllFaceDescriptorsTask(), ComputeSingleFaceDescriptorTask(), extendWithFaceDescriptor(), FaceRecognitionNet()
|
||||
Cohesion: 0.06
|
||||
Nodes (33): boxPredictionLayer(), decodeBoxesLayer(), depthwiseConvLayer(), disposeUnusedWeightTensors(), extractorsFactory$3(), extractorsFactory$5(), extractorsFactory$7(), extractorsFactory$9() (+25 more)
|
||||
|
||||
### Community 23 - "facepp-skinanalyze(旷视皮肤分析专业版调用技能)"
|
||||
Cohesion: 0.20
|
||||
@@ -224,16 +218,16 @@ Cohesion: 0.20
|
||||
Nodes (16): _arrayLikeToArray(), _arrayWithHoles(), _arrayWithoutHoles(), _classCallCheck(), _createClass(), _defineProperties(), h(), _iterableToArray() (+8 more)
|
||||
|
||||
### Community 26 - "wi"
|
||||
Cohesion: 0.28
|
||||
Nodes (9): Ci(), Ei(), Fi(), Ii(), ki(), pe(), ri(), wi() (+1 more)
|
||||
Cohesion: 0.20
|
||||
Nodes (12): Ci(), Ei(), Fi(), Ii(), ki(), pe(), Qt(), ri() (+4 more)
|
||||
|
||||
### Community 27 - "gen_voice.py"
|
||||
Cohesion: 0.30
|
||||
Nodes (11): ndarray, Path, gen_countdown(), main(), normalize_clip(), 倒计时单独按字生成再拼接,避免「三、二、一」整句被读成播报腔、与提示音气质漂移。 与提示音共用同一 speaker + instruct;句间留约…, 裁掉过长首尾静音,保留 ≤150ms 边沿,避免提示显迟钝。, 响度归一 −16 LUFS + 转 MP3 48k CBR mono 22050Hz。 (+3 more)
|
||||
|
||||
### Community 28 - "wn"
|
||||
Cohesion: 0.67
|
||||
Nodes (3): wl(), wn(), xn()
|
||||
### Community 28 - "C"
|
||||
Cohesion: 0.08
|
||||
Nodes (33): bn(), Bo(), C(), cd(), cl(), co(), eo(), fn() (+25 more)
|
||||
|
||||
### Community 29 - "face-api.js 本地化依赖(h5/vendor/faceapi/)"
|
||||
Cohesion: 0.29
|
||||
@@ -247,34 +241,50 @@ Nodes (5): assets, document_size, layers, pages, source
|
||||
Cohesion: 0.60
|
||||
Nodes (4): main(), 递归缩放 result 内全部坐标:{left,top,width,height} 矩形与 {x,y} 点,与原数据同为整数。, scale_coords(), to_data_url()
|
||||
|
||||
### Community 36 - "so"
|
||||
Cohesion: 0.50
|
||||
Nodes (4): Do(), io(), so(), To()
|
||||
### Community 36 - "ao"
|
||||
Cohesion: 0.33
|
||||
Nodes (6): ao(), Do(), io(), oo(), so(), To()
|
||||
|
||||
### Community 37 - "UI 外发版 10.09 图片资源"
|
||||
Cohesion: 0.40
|
||||
Nodes (4): UI 外发版 10.09 图片资源, 前端素材对应, 文件位置, 重新导出
|
||||
|
||||
### Community 38 - "FaceLandmark68Net"
|
||||
Cohesion: 0.17
|
||||
Nodes (3): FaceLandmark68Net(), PredictSingleAgeAndGenderWithFaceAlignmentTask(), PredictSingleFaceExpressionsWithFaceAlignmentTask()
|
||||
|
||||
### Community 39 - "UI交互界面调整"
|
||||
Cohesion: 0.40
|
||||
Nodes (4): 1meAI测肤旷视UI问题反馈, UI交互界面调整, 交互, **设计**
|
||||
|
||||
### Community 40 - "extractAllFacesAndComputeResults"
|
||||
Cohesion: 0.13
|
||||
Nodes (9): extendWithAge(), extendWithFaceExpressions(), extendWithGender(), extractAllFacesAndComputeResults(), extractSingleFaceAndComputeResult(), PredictAllAgeAndGenderTask(), PredictAllFaceExpressionsTask(), PredictSingleAgeAndGenderTask() (+1 more)
|
||||
### Community 40 - ".predictAgeAndGender"
|
||||
Cohesion: 0.28
|
||||
Nodes (4): extendWithAge(), extendWithGender(), PredictAllAgeAndGenderTask(), PredictSingleAgeAndGenderTask()
|
||||
|
||||
### Community 41 - "initialize"
|
||||
Cohesion: 0.29
|
||||
Nodes (7): createBrowserEnv(), createFileSystem(), createNodejsEnv(), initialize(), isBrowser(), monkeyPatch(), setEnv()
|
||||
### Community 41 - "test/accounts.js"
|
||||
Cohesion: 0.22
|
||||
Nodes (9): assert, create, fs, http, os, path, ReportUI, SkinScore (+1 more)
|
||||
|
||||
### Community 42 - "export_psd_assets.py"
|
||||
Cohesion: 0.70
|
||||
Nodes (4): bounds(), layer_at(), main(), save_image()
|
||||
|
||||
### Community 43 - ".loadFromDisk"
|
||||
Cohesion: 0.29
|
||||
Nodes (3): ComposableTask(), getModelUris(), TinyFaceDetector()
|
||||
|
||||
### Community 44 - "start-local.sh"
|
||||
Cohesion: 0.50
|
||||
Nodes (3): HOST, PORT, start-local.sh script
|
||||
|
||||
### Community 47 - "recommendation.js"
|
||||
Cohesion: 0.38
|
||||
Nodes (6): assert, check(), missing, raw, SS, UI
|
||||
|
||||
### Community 48 - "report-persistence.mjs"
|
||||
Cohesion: 0.29
|
||||
Nodes (6): child, dataRoot, fx, h5, ReportUI, require
|
||||
|
||||
### Community 53 - "护理建议(全量文本,按需求摘抄)"
|
||||
Cohesion: 0.10
|
||||
Nodes (19): 5\.1 轮廓(紧塑力), 5\.2 皱纹(紧塑力), 5\.3 肤色(透亮感), 5\.4 平滑度(透亮感), 5\.5 水分(透亮感), 5\.6 泛红(稳韧力), 5\.7 痘痘(稳韧力), 6\.1 轮廓 (+11 more)
|
||||
@@ -283,21 +293,25 @@ 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 56 - "convLayer"
|
||||
Cohesion: 0.15
|
||||
Nodes (17): boxPredictionLayer(), convLayer(), convWithBatchNorm(), depthwiseConvLayer(), depthwiseSeparableConv$1(), fullyConnectedLayer(), getStridesForLayerIdx(), leaky() (+9 more)
|
||||
### Community 55 - "extractFaceTensors"
|
||||
Cohesion: 0.29
|
||||
Nodes (3): DetectAllFaceLandmarksTask(), extractFaceTensors(), isTensor3D()
|
||||
|
||||
### Community 56 - "report-ui.js"
|
||||
Cohesion: 0.90
|
||||
Nodes (4): build(), count(), fill(), numeric()
|
||||
|
||||
### Community 57 - "regression.js"
|
||||
Cohesion: 0.60
|
||||
Nodes (5): diffCtx(), loadJSON(), main(), okNum(), okStr()
|
||||
|
||||
### Community 58 - ".dispose"
|
||||
Cohesion: 0.21
|
||||
Nodes (11): getSizesForScale(), iou(), NetInput(), nonMaxSuppression(), nonMaxSuppression$1(), normalize$1(), rescaleAndNormalize(), scale() (+3 more)
|
||||
Cohesion: 0.16
|
||||
Nodes (12): extractBoundingBoxes(), getSizesForScale(), iou(), nonMaxSuppression(), nonMaxSuppression$1(), normalize$1(), rescaleAndNormalize(), scale() (+4 more)
|
||||
|
||||
### Community 61 - ".extractWeights"
|
||||
Cohesion: 0.09
|
||||
Nodes (25): createFaceDetectionNet(), createFaceRecognitionNet(), createMtcnn(), createSsdMobilenetv1(), createTinyFaceDetector(), createTinyYolov2(), extractConvParamsFactory(), extractFCParamsFactory() (+17 more)
|
||||
### Community 61 - "NeuralNetwork"
|
||||
Cohesion: 0.06
|
||||
Nodes (28): createFaceDetectionNet(), createFaceRecognitionNet(), createMtcnn(), createSsdMobilenetv1(), createTinyFaceDetector(), createTinyYolov2(), extractConvParamsFactory(), extractFCParamsFactory() (+20 more)
|
||||
|
||||
### Community 62 - "提交前 Review(2026-10-11)"
|
||||
Cohesion: 0.40
|
||||
@@ -311,52 +325,52 @@ Nodes (4): EXPECTED, fails, pageErrors, pwRoot
|
||||
Cohesion: 0.90
|
||||
Nodes (4): browser(), init(), read(), request()
|
||||
|
||||
### Community 65 - "tt"
|
||||
Cohesion: 0.50
|
||||
Nodes (4): kn(), tt(), wo(), z()
|
||||
### Community 65 - "k"
|
||||
Cohesion: 0.13
|
||||
Nodes (16): ba(), ca(), fa(), gn(), k(), kn(), pa(), tt() (+8 more)
|
||||
|
||||
### Community 66 - "cn"
|
||||
Cohesion: 0.67
|
||||
Nodes (3): cn(), en(), yn()
|
||||
### Community 66 - "wechat-browser.mjs"
|
||||
Cohesion: 0.40
|
||||
Nodes (4): createAccounts, h5, require, root
|
||||
|
||||
### Community 68 - "analyze-timeout.js"
|
||||
Cohesion: 0.20
|
||||
Nodes (7): assert, end, fs, path, source, start, vm
|
||||
|
||||
### Community 70 - "decodeBoxesLayer"
|
||||
Cohesion: 0.67
|
||||
Nodes (3): decodeBoxesLayer(), getCenterCoordinatesAndSizesLayer(), outputLayer()
|
||||
### Community 71 - "production-loop.mjs"
|
||||
Cohesion: 0.50
|
||||
Nodes (3): artifacts, errors, samples
|
||||
|
||||
### Community 71 - "dr"
|
||||
Cohesion: 0.67
|
||||
Nodes (3): dr(), na(), ta()
|
||||
### Community 77 - "drawFaceExpressions"
|
||||
Cohesion: 0.50
|
||||
Nodes (3): drawFaceExpressions(), FaceExpressions(), isWithFaceExpressions()
|
||||
|
||||
### Community 78 - ".toString"
|
||||
Cohesion: 0.50
|
||||
Nodes (3): FaceMatch(), la(), pr()
|
||||
|
||||
### Community 79 - "isValidNumber"
|
||||
Cohesion: 0.50
|
||||
Nodes (4): Dimensions(), isValidNumber(), isValidProbablitiy(), isWithGender()
|
||||
Cohesion: 0.33
|
||||
Nodes (5): bgrToRgbTensor(), Dimensions(), isValidNumber(), isValidProbablitiy(), isWithGender()
|
||||
|
||||
### Community 80 - "线上微信登录部署记录"
|
||||
Cohesion: 0.29
|
||||
Nodes (6): 初次部署时的凭据查找记录(现已由用户提供), 初次部署的线上检查(历史结果), 后续更新:已配置真实凭据, 已执行, 用户确认加入白名单后的复测, 线上微信登录部署记录
|
||||
|
||||
### Community 83 - "Yo"
|
||||
Cohesion: 0.67
|
||||
Nodes (3): qo(), Xo(), Yo()
|
||||
|
||||
## Knowledge Gaps
|
||||
- **173 isolated node(s):** `source`, `document_size`, `pages`, `assets`, `layers` (+168 more)
|
||||
- **183 isolated node(s):** `source`, `document_size`, `pages`, `assets`, `layers` (+178 more)
|
||||
These have ≤1 connection - possible missing edges or undocumented components.
|
||||
- **18 thin communities (<3 nodes) omitted from report** — run `graphify query` to explore isolated nodes.
|
||||
- **17 thin communities (<3 nodes) omitted from report** — run `graphify query` to explore isolated nodes.
|
||||
|
||||
## Suggested Questions
|
||||
_Questions this graph is uniquely positioned to answer:_
|
||||
|
||||
- **Why does `k()` connect `n` to `face-api.js`, `app.js`, `lib/accounts.js`?**
|
||||
_High betweenness centrality (0.083) - this node is a cross-community bridge._
|
||||
- **Why does `rate()` connect `lib/accounts.js` to `n`?**
|
||||
- **Why does `k()` connect `k` to `face-api.js`, `app.js`, `i`, `n`, `lib/accounts.js`?**
|
||||
_High betweenness centrality (0.061) - this node is a cross-community bridge._
|
||||
- **Why does `renderReport()` connect `app.js` to `n`?**
|
||||
_High betweenness centrality (0.056) - this node is a cross-community bridge._
|
||||
- **Why does `r()` connect `n` to `face-api.js`, `i`, `AgeGenderNet`, `server.js`, `.toString`, `wi`, `C`?**
|
||||
_High betweenness centrality (0.050) - this node is a cross-community bridge._
|
||||
- **Why does `renderReport()` connect `app.js` to `k`, `n`?**
|
||||
_High betweenness centrality (0.047) - this node is a cross-community bridge._
|
||||
- **Are the 24 inferred relationships involving `n()` (e.g. with `newRecordId()` and `ae()`) actually correct?**
|
||||
_`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?**
|
||||
@@ -364,4 +378,4 @@ _Questions this graph is uniquely positioned to answer:_
|
||||
- **Are the 7 inferred relationships involving `t()` (e.g. with `renderReport()` and `e()`) actually correct?**
|
||||
_`t()` has 7 INFERRED edges - model-reasoned connections that need verification._
|
||||
- **What connects `source`, `document_size`, `pages` to the rest of the system?**
|
||||
_173 weakly-connected nodes found - possible documentation gaps or missing edges._
|
||||
_183 weakly-connected nodes found - possible documentation gaps or missing edges._
|
||||
File diff suppressed because one or more lines are too long
+2392
-1626
File diff suppressed because it is too large
Load Diff
+34
-24
@@ -40,8 +40,8 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/js/app.js": {
|
||||
"mtime": 1791730674.7447748,
|
||||
"ast_hash": "7fe1cc5b34a9367c3fed5d502d304e2b",
|
||||
"mtime": 1791735440.264282,
|
||||
"ast_hash": "bd18e424efb5c8d037b64d67f869b198",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/js/head-pose.js": {
|
||||
@@ -50,13 +50,13 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/js/skin-score.js": {
|
||||
"mtime": 1791645379.1321833,
|
||||
"ast_hash": "12fe3e0e5fd4cbe7f0a79a7da01c380b",
|
||||
"mtime": 1791735501.357809,
|
||||
"ast_hash": "783b6c9abbe2bf1832e8eafe5aead7e3",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/server.js": {
|
||||
"mtime": 1791710640.6026413,
|
||||
"ast_hash": "a63e26c0f78becff09cc50e427cb0054",
|
||||
"mtime": 1791735707.6717124,
|
||||
"ast_hash": "6bdce03487aaaf9e3ab62f6b39f7a418",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/build_fixtures.py": {
|
||||
@@ -80,8 +80,8 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/regression.js": {
|
||||
"mtime": 1790528158.6705542,
|
||||
"ast_hash": "d83fc0a378d1117fc2203569d325ca24",
|
||||
"mtime": 1791735501.3654072,
|
||||
"ast_hash": "4a712e295c115b6051726cd028e67be2",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/vendor/faceapi/face-api.js": {
|
||||
@@ -140,8 +140,8 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/index.html": {
|
||||
"mtime": 1791646269.975195,
|
||||
"ast_hash": "c2661595e503e2f166f1be390696ce0c",
|
||||
"mtime": 1791735440.2604818,
|
||||
"ast_hash": "444fca45ad900710607cb15b852c0305",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/vendor/faceapi/README.md": {
|
||||
@@ -815,8 +815,8 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/js/admin.js": {
|
||||
"mtime": 1791556943.863973,
|
||||
"ast_hash": "573a1a971cc3d00a16c8b9d53529f243",
|
||||
"mtime": 1791735707.6717124,
|
||||
"ast_hash": "0de3075fb017762401fd2d27eb26abcb",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/js/region-data.js": {
|
||||
@@ -860,8 +860,8 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/admin.html": {
|
||||
"mtime": 1791556943.863973,
|
||||
"ast_hash": "8dad311cd83b0ec640bcf71ddb9a255c",
|
||||
"mtime": 1791735369.9312572,
|
||||
"ast_hash": "ddfe588d49452e3973e21b080e61dbd7",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/vendor/mobile-select/README.md": {
|
||||
@@ -1245,23 +1245,23 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/js/report-ui.js": {
|
||||
"mtime": 1791646198.024173,
|
||||
"ast_hash": "5cb7f2ac736b6859ed580f5c934b1061",
|
||||
"mtime": 1791735369.923658,
|
||||
"ast_hash": "b05b0a2181711ce6f05bbe6c16179170",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/lib/accounts.js": {
|
||||
"mtime": 1791710640.6026413,
|
||||
"ast_hash": "e36024ccb6c9d6a2d2a0187942d5c65b",
|
||||
"mtime": 1791735369.9274576,
|
||||
"ast_hash": "1a0677e439938aa2e9e4decf38fad568",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/accounts.js": {
|
||||
"mtime": 1791710640.6026413,
|
||||
"ast_hash": "f036faf880172eb33e3d0fd4e5c462f7",
|
||||
"mtime": 1791735546.0452847,
|
||||
"ast_hash": "aa05cb7d924b1ddbe8834faeab24588f",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/report-persistence.mjs": {
|
||||
"mtime": 1791710640.6026413,
|
||||
"ast_hash": "92699686424262cfe208355ce040f236",
|
||||
"mtime": 1791735750.0753446,
|
||||
"ast_hash": "2933add99f8983a590608ba203182096",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/ui-20261009.mjs": {
|
||||
@@ -1280,8 +1280,8 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"docs/10.09\u65b0\u7248H5\u5b9e\u65bd\u4e0e\u9a8c\u6536.md": {
|
||||
"mtime": 1791710850.0228443,
|
||||
"ast_hash": "e3126d7bce54a235d580778609a46dd3",
|
||||
"mtime": 1791735707.6755157,
|
||||
"ast_hash": "6e83c0cd6df4b711c9c526a83f1c33a5",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"docs/UI\u5bf9\u9f5010.09\u8bbe\u8ba1\u7a3f-\u8c03\u7814\u4e0e\u5b9e\u65bd\u8ba1\u5212.md": {
|
||||
@@ -1563,5 +1563,15 @@
|
||||
"mtime": 1791730745.132436,
|
||||
"ast_hash": "04095d09dce4e81349b9256a0e8cb540",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/recommendation.js": {
|
||||
"mtime": 1791735559.0619977,
|
||||
"ast_hash": "589f7046d94485dfc1df20a6129640e0",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"docs/\u4ea7\u54c1\u63a8\u8350\u89c4\u5219-\u80a4\u9f84\u7248.md": {
|
||||
"mtime": 1791735707.6717124,
|
||||
"ast_hash": "69c1278c6c8eea5866982940e20e421c",
|
||||
"semantic_hash": ""
|
||||
}
|
||||
}
|
||||
+7
-5
@@ -44,7 +44,7 @@
|
||||
<option>NETWORK_ERROR</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>年龄 <input type="number" id="f-agemin" min="1" max="120" placeholder="最小"> ~ <input type="number" id="f-agemax" min="1" max="120" placeholder="最大"></label>
|
||||
<label>实际年龄 <input type="number" id="f-agemin" min="1" max="120" placeholder="最小"> ~ <input type="number" id="f-agemax" min="1" max="120" placeholder="最大"></label>
|
||||
<label>产品组别
|
||||
<select id="f-group">
|
||||
<option value="">全部</option>
|
||||
@@ -55,12 +55,12 @@
|
||||
<select id="f-dim">
|
||||
<option value="">不限</option>
|
||||
<option value="firm">紧塑力</option>
|
||||
<option value="vita">活力感</option>
|
||||
<option value="stab">稳定力</option>
|
||||
<option value="vita">透亮感</option>
|
||||
<option value="stab">稳韧力</option>
|
||||
</select>
|
||||
<select id="f-dimlevel">
|
||||
<option value="">不限档位</option>
|
||||
<option>预警</option><option>改善</option><option>良好</option>
|
||||
<option value="预警">改善</option><option value="改善">良好</option><option value="良好">优秀</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>记录ID <input type="text" id="f-q" placeholder="精确/前缀匹配"></label>
|
||||
@@ -72,7 +72,7 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th>照片</th><th>记录ID</th><th>年龄/性别/地区/肤质</th><th>拍摄状态</th><th>上传状态</th>
|
||||
<th>检测状态</th><th>总分</th><th>三维度档位</th><th>推荐精华</th><th>耗时</th><th>创建时间</th>
|
||||
<th>检测状态</th><th>总分</th><th>肌龄(原始 / 展示)</th><th>三维度评分 / 档位</th><th>推荐精华</th><th>耗时</th><th>创建时间</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
@@ -149,6 +149,8 @@
|
||||
|
||||
<div id="toast" class="toast hidden"></div>
|
||||
|
||||
<script src="js/report-copy.js"></script>
|
||||
<script src="js/report-ui.js"></script>
|
||||
<script src="js/admin.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+1
-1
@@ -62,7 +62,7 @@ tbody tr:last-child td { border-bottom: none; }
|
||||
.mono { font-family: ui-monospace, Consolas, monospace; font-size: 12px; color: #4a5568; }
|
||||
.sub { color: #8a94a6; font-size: 12px; }
|
||||
|
||||
/* 档位标签:预警=红 / 改善=橙黄 / 良好=绿 */
|
||||
/* 原始档位样式键不变;展示按 ReportUI 映射为改善 / 良好 / 优秀。 */
|
||||
.lv {
|
||||
display: inline-block; padding: 2px 8px; border-radius: 10px; font-size: 12px; margin: 1px 2px;
|
||||
}
|
||||
|
||||
@@ -261,6 +261,7 @@
|
||||
|
||||
<!-- 产品推荐大标题独立于白卡之外,直接落在灰底上(Figma 08) -->
|
||||
<h2 class="prod-title">感受高光时刻<br>从日常的好状态开始</h2>
|
||||
<p id="product-unavailable" class="care-item" hidden>本次未获取有效肌龄,暂无法推荐产品,建议重新检测。</p>
|
||||
<div class="card card-product">
|
||||
<div class="prod-card">
|
||||
<div class="prod-info">
|
||||
|
||||
+56
-19
@@ -24,7 +24,7 @@ function fmtMs(ms) { return ms == null ? '-' : ms + 'ms'; }
|
||||
function fmtScore(v) { return v == null ? '-' : v; }
|
||||
|
||||
var LV_CLASS = { '预警': 'lv-bad', '改善': 'lv-warn', '良好': 'lv-good' };
|
||||
var DIM_NAMES = { firm: '紧塑力', vita: '活力感', stab: '稳定力' };
|
||||
var DIM_NAMES = ReportUI.dimNames;
|
||||
var CAT_ORDER = ['contour', 'wrinkles', 'smooth', 'tone', 'sens', 'acne'];
|
||||
var ST_CLASS = { '成功': 'st-ok', '失败': 'st-fail', '检测中': 'st-ing', '待检测': 'st-wait' };
|
||||
|
||||
@@ -60,15 +60,16 @@ function switchTab(name) {
|
||||
/* ─── 档位标签 ─── */
|
||||
function lvHtml(level) {
|
||||
if (!level) return '<span class="sub">-</span>';
|
||||
return '<span class="lv ' + (LV_CLASS[level] || '') + '">' + esc(level) + '</span>';
|
||||
return '<span class="lv ' + (LV_CLASS[level] || '') + '">' + esc(ReportUI.levelMap[level] || level) + '</span>';
|
||||
}
|
||||
function dimsHtml(dims) {
|
||||
if (!dims) return '<span class="sub">-</span>';
|
||||
var h = '';
|
||||
for (var k in DIM_NAMES) {
|
||||
var d = dims[k];
|
||||
h += '<span class="lv ' + (d && LV_CLASS[d.level] || '') + '" title="' + DIM_NAMES[k] + '">' +
|
||||
DIM_NAMES[k].slice(0, 1) + (d ? esc(d.level) : '-') + '</span>';
|
||||
h += '<div><span class="lv ' + (d && LV_CLASS[d.level] || '') + '" title="' + DIM_NAMES[k] + '">' +
|
||||
DIM_NAMES[k] + ' ' + (d && typeof d.S === 'number' ? d.S.toFixed(1) : '-') + ' ' +
|
||||
(d ? esc(ReportUI.levelMap[d.level] || '-') : '-') + '</span></div>';
|
||||
}
|
||||
return h;
|
||||
}
|
||||
@@ -80,6 +81,19 @@ function profileHtml(it) {
|
||||
parts.push(it.daily_skin_type || '-');
|
||||
return esc(parts.join(' / '));
|
||||
}
|
||||
function ageText(age) { return age == null ? '-' : esc(age) + '岁'; }
|
||||
function skinAgeHtml(r) {
|
||||
return '<b>原始 ' + ageText(r.skin_age) + '</b><div class="sub">展示 ' + ageText(r.skin_age_display) + '</div>';
|
||||
}
|
||||
function productName(name) {
|
||||
for (var i = 0; i < ReportUI.products.length; i++) {
|
||||
var p = ReportUI.products[i];
|
||||
if (p.match === name) return p.title + '(' + ['25+', '35+', '50+'][i] + ')';
|
||||
}
|
||||
return name || '-';
|
||||
}
|
||||
function ruleLabel(r) { return r.recommendation ? r.recommendation.version : '旧版规则(历史记录)'; }
|
||||
function tierText(tier) { return tier ? ['25+ / I', '35+ / II', '50+ / III'][tier - 1] : '无'; }
|
||||
|
||||
/* ═══════════ 记录列表 ═══════════ */
|
||||
var pageState = { page: 1, page_size: 20, total: 0 };
|
||||
@@ -108,7 +122,7 @@ function loadRecords() {
|
||||
var tbody = $('#tbl-records tbody');
|
||||
tbody.innerHTML = '';
|
||||
if (!data.items.length) {
|
||||
tbody.innerHTML = '<tr><td colspan="11" style="text-align:center;color:#8a94a6;padding:32px">暂无记录</td></tr>';
|
||||
tbody.innerHTML = '<tr><td colspan="12" style="text-align:center;color:#8a94a6;padding:32px">暂无记录</td></tr>';
|
||||
}
|
||||
for (var i = 0; i < data.items.length; i++) {
|
||||
(function (it) {
|
||||
@@ -126,8 +140,9 @@ function loadRecords() {
|
||||
'<td><span class="st ' + (ST_CLASS[it.detect_status] || '') + '">' + esc(it.detect_status) + '</span>' +
|
||||
(it.fail_code ? '<div class="sub">' + esc(it.fail_code) + '</div>' : '') + '</td>' +
|
||||
'<td><b>' + fmtScore(it.total_score) + '</b></td>' +
|
||||
'<td>' + skinAgeHtml(it) + '</td>' +
|
||||
'<td>' + dimsHtml(it.dims) + '</td>' +
|
||||
'<td>' + esc(it.essence || '-') + '</td>' +
|
||||
'<td>' + esc(productName(it.essence)) + '<div class="sub">' + esc(ruleLabel(it)) + '</div></td>' +
|
||||
'<td class="mono">' + fmtMs(it.cost_ms) + '</td>' +
|
||||
'<td class="mono">' + fmtTime(it.created_at) + '</td>';
|
||||
tr.addEventListener('click', function (e) {
|
||||
@@ -230,7 +245,7 @@ function loadStats() {
|
||||
for (var l = 0; l < levels.length; l++) {
|
||||
var cnt = dist[levels[l][0]] || 0;
|
||||
bars += '<div style="display:flex;align-items:center;gap:8px;margin:3px 0">' +
|
||||
'<span style="width:34px;color:#5a6578;font-size:12px">' + levels[l][0] + '</span>' +
|
||||
'<span style="width:34px;color:#5a6578;font-size:12px">' + ReportUI.levelMap[levels[l][0]] + '</span>' +
|
||||
'<span style="flex:1;height:14px;background:#f0f2f5;border-radius:3px;overflow:hidden">' +
|
||||
'<i style="display:block;height:100%;width:' + (cnt * 100 / max) + '%" class="' + levels[l][1] + '"></i></span>' +
|
||||
'<span style="width:24px;text-align:right;font-size:12px;color:#4a5568">' + cnt + '</span></div>';
|
||||
@@ -342,7 +357,7 @@ function renderDetail(data) {
|
||||
|
||||
/* 档案 */
|
||||
h += dSec('档案信息', '<div class="d-kv">' +
|
||||
'<span>年龄 <b>' + (r.real_age != null ? r.real_age + '岁' : '-') + '</b></span>' +
|
||||
'<span>实际年龄 <b>' + ageText(r.real_age) + '</b></span>' +
|
||||
'<span>性别 <b>' + esc(r.gender || '-') + '</b></span>' +
|
||||
'<span>地区 <b>' + esc(r.region || '-') + '</b></span>' +
|
||||
'<span>日常肤质 <b>' + esc(r.daily_skin_type || '-') + '</b></span>' +
|
||||
@@ -374,20 +389,22 @@ function renderDetail(data) {
|
||||
if (r.detect_status === '成功') {
|
||||
h += dSec('综合', '<div class="d-kv">' +
|
||||
'<span>总分 <b>' + fmtScore(r.total_score) + '</b></span>' +
|
||||
'<span>肌肤年龄(接口原始) <b>' + fmtScore(r.skin_age) + '岁</b></span>' +
|
||||
'<span>原始肌龄(新推荐依据) <b>' + ageText(r.skin_age) + '</b></span>' +
|
||||
'<span>前端展示肌龄 <b>' + ageText(r.skin_age_display) + '</b></span>' +
|
||||
'<span>六分类均分S <b>' + fmtScore(r.avg_s) + '</b></span>' +
|
||||
'<span>左下颌线角 <b>' + (r.jawline_angle_l != null ? r.jawline_angle_l + '°' : '-') + '</b></span>' +
|
||||
'<span>右下颌线角 <b>' + (r.jawline_angle_r != null ? r.jawline_angle_r + '°' : '-') + '</b></span>' +
|
||||
'</div>');
|
||||
h += '<p class="sub">前端展示肌龄沿用实际年龄±3岁的修正;新推荐只使用原始肌龄。历史报告保留当时的推荐。</p>';
|
||||
|
||||
/* 6 分类表格 */
|
||||
var catRows = '';
|
||||
for (var i = 0; i < CAT_ORDER.length; i++) {
|
||||
var ck = CAT_ORDER[i], cat = r.cats && r.cats[ck];
|
||||
var catName = (cat && cat.name) || ({ contour: '轮廓', wrinkles: '皱纹', smooth: '光滑', tone: '肤色水润', sens: '敏感', acne: '痘痘' })[ck];
|
||||
var catName = ({ contour: '轮廓', wrinkles: '皱纹', smooth: '平滑度', tone: '肤色 / 水分', sens: '泛红', acne: '痘痘' })[ck];
|
||||
catRows += '<tr><td>' + esc(catName) + '</td><td>' + fmtScore(cat && cat.P) + '</td><td>' + fmtScore(cat && cat.S) + '</td><td>' + lvHtml(cat && cat.level) + '</td></tr>';
|
||||
}
|
||||
h += dSec('六分类评分', '<table class="d-table"><thead><tr><th>分类</th><th>P</th><th>S</th><th>档位</th></tr></thead><tbody>' + catRows + '</tbody></table>');
|
||||
h += dSec('原始六分类评分(肤色 / 水分共用一项)', '<table class="d-table"><thead><tr><th>分类</th><th>P</th><th>S</th><th>档位</th></tr></thead><tbody>' + catRows + '</tbody></table>');
|
||||
|
||||
/* 3 维度表格 */
|
||||
var dimRows = '';
|
||||
@@ -399,7 +416,13 @@ function renderDetail(data) {
|
||||
|
||||
/* 文案全文 */
|
||||
var copyHtml = '';
|
||||
if (r.copy_6) {
|
||||
var presentation = r.presentation;
|
||||
if (presentation && presentation.views) {
|
||||
for (var v = 0; v < presentation.views.length; v++) {
|
||||
var view = presentation.views[v];
|
||||
copyHtml += '<div class="d-copy"><b>' + esc(view.name) + ' / ' + esc(view.level || '未获取') + ':</b>' + esc(view.text) + '</div>';
|
||||
}
|
||||
} else if (r.copy_6) {
|
||||
for (var c2 = 0; c2 < CAT_ORDER.length; c2++) {
|
||||
var txt = r.copy_6[CAT_ORDER[c2]];
|
||||
if (txt) copyHtml += '<div class="d-copy"><b>' + esc((r.cats[CAT_ORDER[c2]] || {}).name || '') + ':</b>' + esc(txt) + '</div>';
|
||||
@@ -407,19 +430,33 @@ function renderDetail(data) {
|
||||
}
|
||||
if (r.copy_summary) copyHtml += '<div class="d-copy"><b>综合文案:</b>' + esc(r.copy_summary) + '</div>';
|
||||
if (r.lifestyle) copyHtml += '<div class="d-copy"><b>生活方式:</b>' + esc(r.lifestyle) + '</div>';
|
||||
if (copyHtml) h += dSec('生成文案', copyHtml);
|
||||
if (copyHtml) h += dSec(presentation ? '前端报告结论(保存快照)与计算摘要' : '旧版生成文案(无前端快照)', copyHtml);
|
||||
|
||||
if (r.careAdvice && r.careAdvice.length) {
|
||||
var care = presentation ? presentation.care : r.careAdvice;
|
||||
if (care && care.length) {
|
||||
var advice = '';
|
||||
for (var a = 0; a < r.careAdvice.length; a++) {
|
||||
advice += '<div class="d-copy"><b>[' + esc(r.careAdvice[a].tag) + ']</b>' + esc(r.careAdvice[a].text) + '</div>';
|
||||
for (var a = 0; a < care.length; a++) {
|
||||
advice += '<div class="d-copy"><b>[' + esc(care[a].name || care[a].tag) + ']</b>' + esc(care[a].text) + '</div>';
|
||||
}
|
||||
h += dSec('护理建议', advice);
|
||||
}
|
||||
|
||||
h += dSec('推荐精华', '<div class="d-copy"><b>' + esc((r.essence && r.essence.name) || '-') + '</b><br>' +
|
||||
esc((r.essence && r.essence.text) || '') + '</div>' +
|
||||
'<div class="d-kv"><span>触发判据 <b>' + esc(r.trigger || '-') + '</b></span></div>');
|
||||
var product = presentation && presentation.product;
|
||||
h += dSec('推荐精华', '<div class="d-copy"><b>' + esc(productName(r.essence && r.essence.name)) + '</b><br>' +
|
||||
esc(product ? product.name + '双仓精华液' : (r.essence && r.essence.text) || '') + '</div>' +
|
||||
'<div class="d-kv"><span>规则版本 <b>' + esc(ruleLabel(r)) + '</b></span>' +
|
||||
'<span>触发判据 <b>' + esc(r.trigger || '-') + '</b></span></div>');
|
||||
if (r.recommendation) {
|
||||
var rec = r.recommendation, scoreParts = [];
|
||||
for (var sk in DIM_NAMES) {
|
||||
var score = rec.scores[sk];
|
||||
scoreParts.push(DIM_NAMES[sk] + ':' + (score == null ? '未获取' : String(score)));
|
||||
}
|
||||
h += dSec('推荐依据', '<div class="d-kv"><span>使用原始肌龄 <b>' + ageText(rec.skinAge) + '</b></span>' +
|
||||
'<span>基础档 <b>' + esc(tierText(rec.baseTier)) + '</b></span>' +
|
||||
'<span>最终档 <b>' + esc(tierText(rec.tier)) + '</b></span></div>' +
|
||||
'<div class="d-copy">' + esc(rec.reason) + '</div><div class="sub">判定分(未舍入S,不使用前端一位小数):' + esc(scoreParts.join(';')) + '</div>');
|
||||
}
|
||||
|
||||
if (r.notes && r.notes.length) {
|
||||
var notes = '<ul class="d-notes">';
|
||||
|
||||
@@ -1194,6 +1194,7 @@
|
||||
var product = presentation.product;
|
||||
$('.card-product').hidden = !product;
|
||||
$('.prod-title').hidden = !product;
|
||||
$('#product-unavailable').hidden = !!product;
|
||||
if (product) {
|
||||
$('#product-title').textContent = product.title;
|
||||
$('#product-name').textContent = product.name + '双仓精华液';
|
||||
|
||||
+3
-2
@@ -15,6 +15,7 @@
|
||||
{ tier: 3, match: '50+重塑精华', title: "1'me 主场精华 III", name: '以我从容有态构塑', image: 'assets/img/ui-20261009/product-50.png' }
|
||||
];
|
||||
var levelMap = { '预警': '改善', '改善': '良好', '良好': '优秀' };
|
||||
var dimNames = { firm: '紧塑力', vita: '透亮感', stab: '稳韧力' };
|
||||
function numeric(value) { return typeof value === 'number' && isFinite(value); }
|
||||
function fill(template, vars) {
|
||||
return template.replace(/\{([a-z_]+)\}/g, function(_, key) {
|
||||
@@ -66,7 +67,7 @@
|
||||
var chosen = views.filter(function(v) { return ranked.indexOf(v.source) >= 0 && v.advice; });
|
||||
var care = chosen.map(function(v) { return { name: v.name, text: v.advice.care + v.advice.lifestyle }; });
|
||||
var product = products.filter(function(p) { return p.match === vm.essence.name; })[0] || null;
|
||||
return { version: Copy.version, dims: { firm: '紧塑力', vita: '透亮感', stab: '稳韧力' }, views: views, care: care, product: product };
|
||||
return { version: Copy.version, dims: dimNames, views: views, care: care, product: product };
|
||||
}
|
||||
return { build: build, products: products, levelMap: levelMap };
|
||||
return { build: build, products: products, levelMap: levelMap, dimNames: dimNames };
|
||||
});
|
||||
|
||||
+72
-39
@@ -1,5 +1,6 @@
|
||||
/* ═════ 巨子测肤 H5 — 三维度×6分类评分引擎(纯逻辑,浏览器/Node 双端) ═════
|
||||
* 最高契约:docs/三维度评分与话术生成需求文档_V2.2.docx(公式/锁值/话术/产品推荐以其为准)
|
||||
* 评分契约:docs/三维度评分与话术生成需求文档_V2.2.docx(公式/锁值/原始话术)
|
||||
* 推荐覆盖契约:docs/产品推荐规则-肤龄版.md(2026-10-12 用户确认)
|
||||
* 浏览器:window.SkinScore;Node:module.exports。ES5、零依赖、不碰 DOM。
|
||||
*
|
||||
* 字段口径(已对 reference/facepp-skinanalyze/examples/ 真实 pro JSON 复核):
|
||||
@@ -63,7 +64,7 @@
|
||||
function round2(x) { return Math.round((x + 1e-9) * 100) / 100; } // 1e-9 修正 9.445 类浮点
|
||||
function sRaw(P) { return 7 + 3 * P / 100; }
|
||||
function sShow(P) { // 展示层钳制:P 可为负(肤色扣分/大面积敏感),S 会 <7,与文档「S 7.0–10.0」口径矛盾;
|
||||
if (P == null) return null; // 仅钳 viewModel 展示值,判档/最差项/精华三重判据仍用未舍入原值
|
||||
if (P == null) return null; // 仅钳展示值,推荐边界比较仍用未舍入维度 S 原值
|
||||
var s = sRaw(P);
|
||||
return round2(s < 7 ? 7 : (s > 10 ? 10 : s));
|
||||
}
|
||||
@@ -589,6 +590,47 @@
|
||||
3: { name: '50+重塑精华', text: '建议搭配使用50+重塑精华:针对熟龄肌松弛与轮廓流失问题,深层重塑支撑结构,改善明显松弛与纹路,重建面部紧实与弹性。' }
|
||||
};
|
||||
|
||||
// 2026-10-12 已确认的业务规则。只接收原始肤龄和三维度未舍入 S,
|
||||
// 不使用实际年龄、展示肤龄、六分类均分或最差分类;历史快照不重算。
|
||||
function recommendProduct(skinAge, scores) {
|
||||
scores = scores || {};
|
||||
var firm = isNum(scores.firm) ? scores.firm : null;
|
||||
var vita = isNum(scores.vita) ? scores.vita : null;
|
||||
var stab = isNum(scores.stab) ? scores.stab : null;
|
||||
var rec = { version: '2026-10-12-skin-age-v1', skinAge: skinAge,
|
||||
baseTier: null, tier: null, boundary: null, adjustment: 'none',
|
||||
scores: { firm: firm, vita: vita, stab: stab }, reason: '', triggers: [] };
|
||||
if (!isNum(skinAge) || skinAge <= 0) {
|
||||
rec.adjustment = 'unavailable';
|
||||
rec.reason = '未获取有效原始肌龄,暂不推荐具体产品;不以实际年龄代替。';
|
||||
return rec;
|
||||
}
|
||||
rec.baseTier = skinAge < 35 ? 1 : (skinAge < 50 ? 2 : 3);
|
||||
rec.tier = rec.baseTier;
|
||||
rec.triggers.push('肤龄基础分组');
|
||||
if (skinAge >= 32 && skinAge <= 38) rec.boundary = 35;
|
||||
else if (skinAge >= 47 && skinAge <= 53) rec.boundary = 50;
|
||||
if (rec.boundary != null && skinAge < rec.boundary && firm != null && firm < 8) {
|
||||
rec.tier++;
|
||||
rec.adjustment = 'up';
|
||||
rec.reason = '处于' + rec.boundary + '岁下侧边界,紧塑力低于8分,升一档。';
|
||||
rec.triggers.push(rec.boundary + '岁边界低分升档');
|
||||
} else if (rec.boundary != null && skinAge >= rec.boundary && firm != null && firm >= 9
|
||||
&& ((vita != null && vita >= 9) || (stab != null && stab >= 9))) {
|
||||
rec.tier--;
|
||||
rec.adjustment = 'down';
|
||||
rec.reason = '处于' + rec.boundary + '岁上侧边界,紧塑力≥9且透亮感或稳韧力≥9,降一档。';
|
||||
rec.triggers.push(rec.boundary + '岁边界高分降档');
|
||||
} else if (skinAge < 32) {
|
||||
rec.reason = '原始肌龄小于32岁,固定推荐25+,轮廓或皱纹低分不升档。';
|
||||
} else if (rec.boundary != null) {
|
||||
rec.reason = '处于' + rec.boundary + '岁边界,未满足升降档条件,保持肤龄基础档。';
|
||||
} else {
|
||||
rec.reason = '不在35±3或50±3岁边界内,仅按原始肌龄推荐。';
|
||||
}
|
||||
return rec;
|
||||
}
|
||||
|
||||
/* ─── V2.3 §8 护理建议 ─── */
|
||||
var SKIN_TYPE_TEXT = { // §8.6 五类映射【肤质定制】段全文 + 联动句
|
||||
'干性肌肤': { base: '你的日常肤质为干性肌肤:全脸皮脂分泌偏少,角质层保水能力弱,易紧绷起屑。护理以高效保湿与封闭锁水为核心,洁面选用温和低泡产品,避免频繁去角质,面霜可选择含神经酰胺、角鲨烷等封闭性成分的配方。',
|
||||
@@ -651,14 +693,12 @@
|
||||
var skinAge = val(r.skin_age);
|
||||
var skinAgeDisplay = skinAge, ageClampNote = '';
|
||||
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 group = null;
|
||||
if (realAge != null) group = realAge < 32 ? '25+组' : (realAge <= 45 ? '35+组' : '50+组');
|
||||
var skinTypeCn = skinTypeCnOf(r, profile); // {skin_type_cn}:接口枚举优先,回退用户自选肤质
|
||||
|
||||
// 6 分类
|
||||
@@ -707,25 +747,16 @@
|
||||
if (!worst || cP < worst.P) worst = { catKey: def.key, name: def.name, P: cP, S: sShow(cP) };
|
||||
}
|
||||
|
||||
// 精华推荐:三重判据取最强档,triggers 记录命中(档位=最终档)的判据
|
||||
var crits = []; // {name, tier}
|
||||
if (avgRaw != null) crits.push({ name: '均分档', tier: avgRaw >= 9 ? 1 : (avgRaw >= 8 ? 2 : 3) });
|
||||
if (worst) {
|
||||
var wS = sRaw(worst.P);
|
||||
if (wS < 7) crits.push({ name: '最差项约束', tier: 3 });
|
||||
else if (wS < 8) crits.push({ name: '最差项约束', tier: 2 });
|
||||
}
|
||||
if (group) crits.push({ name: '年龄组档', tier: group === '25+组' ? 1 : (group === '35+组' ? 2 : 3) });
|
||||
var tier = 1;
|
||||
if (crits.length) {
|
||||
tier = crits[0].tier;
|
||||
for (i = 0; i < crits.length; i++) tier = Math.max(tier, crits[i].tier); // 推荐强度只升不降
|
||||
} else {
|
||||
notes.push('三重判据均不可用,精华推荐缺省为 25+束紧精华');
|
||||
}
|
||||
var triggers = [];
|
||||
for (i = 0; i < crits.length; i++) if (crits[i].tier === tier) triggers.push(crits[i].name);
|
||||
var essence = { name: ESSENCE[tier].name, text: ESSENCE[tier].text, triggers: triggers };
|
||||
var recommendation = recommendProduct(skinAge, {
|
||||
firm: dims.firm.P == null ? null : sRaw(dims.firm.P),
|
||||
vita: dims.vita.P == null ? null : sRaw(dims.vita.P),
|
||||
stab: dims.stab.P == null ? null : sRaw(dims.stab.P)
|
||||
});
|
||||
var tier = recommendation.tier, triggers = recommendation.triggers;
|
||||
var group = tier == null ? null : ['25+组', '35+组', '50+组'][tier - 1];
|
||||
var essence = { name: tier == null ? '暂无法推荐' : ESSENCE[tier].name,
|
||||
text: tier == null ? recommendation.reason : ESSENCE[tier].text, tier: tier, triggers: triggers };
|
||||
if (tier == null) notes.push(recommendation.reason);
|
||||
|
||||
// 近期优先关注:最差维度
|
||||
var focus = { name: '', tip: '' }, fP = null;
|
||||
@@ -764,7 +795,7 @@
|
||||
], ',');
|
||||
summary = '【综合】' + (head ? head + ';' : '')
|
||||
+ '六分类S均分' + fmtS(avgRaw) + (worst ? '、最差分类' + worst.name + ' S=' + fmtS(sRaw(worst.P)) : '') + '。'
|
||||
+ '按三重判据' + (triggers.length ? '(触发:' + triggers.join('、') + ')' : '') + '推荐' + essence.name
|
||||
+ (tier == null ? recommendation.reason : '按肤龄及边界规则(触发:' + triggers.join('、') + ')推荐' + essence.name)
|
||||
+ (improve ? ':' + improve : '。')
|
||||
+ (tail ? tail + '。' : '');
|
||||
}
|
||||
@@ -795,6 +826,7 @@
|
||||
cats: cats,
|
||||
group: group,
|
||||
essence: essence,
|
||||
recommendation: recommendation,
|
||||
avgS: avgS,
|
||||
worst: worst,
|
||||
focus: focus,
|
||||
@@ -868,6 +900,7 @@
|
||||
|
||||
return {
|
||||
buildViewModel: buildViewModel,
|
||||
recommendProduct: recommendProduct,
|
||||
MOCK_RESULT: MOCK_RESULT,
|
||||
MOCK_SIDE: MOCK_SIDE
|
||||
};
|
||||
@@ -909,14 +942,14 @@ if (typeof require === 'function' && typeof module !== 'undefined' && require.ma
|
||||
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.skinAgeRaw === 20 && vm.skinAgeDisplay === 22 && vm.ageClampNote === '触发(原20→22)', '肌肤年龄钳制 20→22 留痕,原值不改写');
|
||||
ok(vm.group === '25+组', 'realAge=25 → 25+组');
|
||||
ok(vm.essence.name === '35+支撑精华' && vm.essence.triggers.length === 1 && vm.essence.triggers[0] === '最差项约束',
|
||||
'精华三重判据:均分9.32→25+、最差S7.69→至少35+、年龄→25+,取最强档35+,triggers=["最差项约束"]');
|
||||
ok(vm.group === '25+组', '原始肌龄20 → 25+组');
|
||||
ok(vm.essence.name === '25+束紧精华' && vm.recommendation.tier === 1,
|
||||
'肌龄小于32,即使皱纹低分也固定25+,不再使用最差项升档');
|
||||
ok(vm.avgS === 9.32, '六分类S均分 9.32');
|
||||
ok(vm.worst.catKey === 'wrinkles' && vm.worst.P === 23, '最差分类 = 皱纹');
|
||||
ok(vm.focus.name === '紧塑力', '近期优先关注 = 最差维度紧塑力');
|
||||
ok(vm.total === 79, '综合评分 = score_info.total_score');
|
||||
ok(vm.summary.indexOf('【综合】') === 0 && vm.summary.indexOf('35+支撑精华') > 0, '综合文案生成');
|
||||
ok(vm.summary.indexOf('【综合】') === 0 && vm.summary.indexOf('25+束紧精华') > 0, '综合文案生成');
|
||||
ok(vm.cats.smooth.text.indexOf('粗大毛孔296颗、黑头39颗') >= 0 && vm.cats.smooth.text.indexOf('纹理分87') >= 0,
|
||||
'光滑话术含粗大毛孔/黑头/纹理分变量(分区计数 4 区求和 296)');
|
||||
checkTexts(vm, 'T0');
|
||||
@@ -939,7 +972,7 @@ if (typeof require === 'function' && typeof module !== 'undefined' && require.ma
|
||||
ok(vm2.dims.firm.P === Math.min(vm2.cats.contour.P, vm2.cats.wrinkles.P)
|
||||
&& vm2.dims.vita.P === Math.min(vm2.cats.smooth.P, vm2.cats.tone.P)
|
||||
&& vm2.dims.stab.P === Math.min(vm2.cats.sens.P, vm2.cats.acne.P), '三维度 P = min(旗下两分类)');
|
||||
ok(vm2.group === '35+组', 'realAge=40 → 35+组');
|
||||
ok(vm2.group === '25+组', '实际年龄40不影响推荐,原始肌龄20仍为25+组');
|
||||
|
||||
console.log('== T3 钳制边界 ==');
|
||||
var r3 = clone(SS.MOCK_RESULT);
|
||||
@@ -950,7 +983,7 @@ if (typeof require === 'function' && typeof module !== 'undefined' && require.ma
|
||||
var vm3b = SS.buildViewModel(r3, SS.MOCK_SIDE, { realAge: 25 });
|
||||
ok(vm3b.skinAgeDisplay === 28 && vm3b.ageClampNote === '触发(原40→28)' && vm3b.skinAgeRaw === 40, '上边界钳制 40→28');
|
||||
|
||||
console.log('== T4 精华三重判据:最差项 S<7 → 50+重塑 ==');
|
||||
console.log('== T4 最差项不再覆盖肤龄推荐 ==');
|
||||
var r4 = clone(SS.MOCK_RESULT);
|
||||
r4.score_info.melanin_score = 0;
|
||||
r4.score_info.water_score = 0;
|
||||
@@ -959,8 +992,8 @@ if (typeof require === 'function' && typeof module !== 'undefined' && require.ma
|
||||
r4.water = { water_score: 0, water_area: 40, water_severity: 70 }; // sev≥60 但 score<80,不触发换算
|
||||
var vm4 = SS.buildViewModel(r4, SS.MOCK_SIDE, { realAge: 25 });
|
||||
ok(approx(vm4.cats.tone.P, -7.5), '肤色水润P = 0.5×(0−10−5)+0.5×0 = −7.5(P 原值不动,不夹逼)');
|
||||
ok(vm4.cats.tone.S === 7, '展示层 S 钳到 7(未钳原值 6.78 仍参与判档)');
|
||||
ok(vm4.essence.name === '50+重塑精华' && vm4.essence.triggers.indexOf('最差项约束') >= 0, '最差项 S 原值<7 → 50+重塑精华,triggers 含最差项约束(三重判据用未舍入原值)');
|
||||
ok(vm4.cats.tone.S === 7, '展示层 S 钳到7,评分公式不变');
|
||||
ok(vm4.essence.name === '25+束紧精华', '原始肌龄20,即使最差项S<7也不升档');
|
||||
checkTexts(vm4, 'T4');
|
||||
|
||||
console.log('== T5 缺数据降级(V2.2 第十章异常与边界) ==');
|
||||
@@ -977,9 +1010,9 @@ if (typeof require === 'function' && typeof module !== 'undefined' && require.ma
|
||||
var vm5b = SS.buildViewModel(clone(SS.MOCK_RESULT), null, { realAge: 25 });
|
||||
ok(vm5b.cats.contour.P === 100 && vm5b.notes.join('|').indexOf('侧脸') >= 0, '无侧脸 → 轮廓 P 由眼袋单独决定并记 note');
|
||||
var vm5c = SS.buildViewModel(clone(SS.MOCK_RESULT), SS.MOCK_SIDE, {});
|
||||
ok(vm5c.group === null && vm5c.skinAgeDisplay === 20 && vm5c.ageClampNote === ''
|
||||
ok(vm5c.group === '25+组' && vm5c.skinAgeDisplay === 20 && vm5c.ageClampNote === ''
|
||||
&& vm5c.essence.triggers.indexOf('年龄组档') < 0 && vm5c.notes.join('|').indexOf('实际年龄') >= 0,
|
||||
'realAge 缺失 → group=null、不钳制、判据③不生效并记 note');
|
||||
'实际年龄缺失不钳制,但原始肌龄仍可推荐25+');
|
||||
checkTexts(vm5, 'T5');
|
||||
|
||||
console.log('== T6 下颌线 quality 降级 ==');
|
||||
@@ -1138,8 +1171,8 @@ if (typeof require === 'function' && typeof module !== 'undefined' && require.ma
|
||||
ok(vm.cats.sens.P === 99 && vm.cats.acne.P === 70 && vm.dims.stab.P === 70 && vm.dims.stab.S === 9.1,
|
||||
'敏感 99 / 痘痘 70(min(分级70, 100−12)),稳定力 70 / S=9.1');
|
||||
ok(vm.avgS === 9.05 && vm.worst.catKey === 'wrinkles' && vm.group === '25+组'
|
||||
&& vm.essence.name === '35+支撑精华' && vm.essence.triggers.join('、') === '最差项约束',
|
||||
'均分 9.05 / 最差皱纹 / 25+组 / 精华 35+支撑(触发:最差项约束,三重判据取最强档)');
|
||||
&& vm.essence.name === '25+束紧精华' && vm.essence.triggers.join('、') === '肤龄基础分组',
|
||||
'评分不变,原始肌龄27推荐25+,不再因最差皱纹升档');
|
||||
ok(vm.skinAgeRaw === 27 && vm.skinAgeDisplay === 27 && vm.ageClampNote === '', '肤龄 27(实际30)未触发钳制、原值不改写');
|
||||
ok(vm.cats.contour.text === '下颌线角度左104.54°/右103.75°(标准区间116–120°,双侧均偏离标准,提示下颌缘支撑力下降);眼袋1级、黑眼圈1级;中面部支撑与轮廓清晰度处于预警水平。建议加强轮廓紧致护理:重点针对下颌线与下颌缘区域使用紧致类精华,配合由下向上的提拉按摩手法,同时加强眼周支撑护理,将紧致防线前置。',
|
||||
'轮廓文案逐字 = 真值表(黑眼圈取 dark_circle_severity=1,非 dark_circle=3)');
|
||||
@@ -1153,8 +1186,8 @@ if (typeof require === 'function' && typeof module !== 'undefined' && require.ma
|
||||
'敏感文案逐字 = 真值表');
|
||||
ok(vm.cats.acne.text === '检出9处皮损(闭口粉刺6处、丘疹3处)。建议尽快进行针对性抗痘护理,避免挤压与刺激,必要时寻求皮肤科专业处理。',
|
||||
'痘痘文案逐字 = 真值表(无 heavy_note)');
|
||||
ok(vm.summary === '【综合】实际年龄30岁(接口测得肌肤年龄27岁,差3岁),产品组别25+组;六分类S均分9.05、最差分类皱纹 S=7.69。按三重判据(触发:最差项约束)推荐35+支撑精华:优先改善皱纹(P=23.0)与光滑(P=57.2),其余维度维持现有护理节奏。整体肤质混合性,T区出油面积57%、左右脸颊20%/2%。',
|
||||
'综合文案逐字 = 真值表 AW 列');
|
||||
ok(vm.summary === '【综合】实际年龄30岁(接口测得肌肤年龄27岁,差3岁),产品组别25+组;六分类S均分9.05、最差分类皱纹 S=7.69。按肤龄及边界规则(触发:肤龄基础分组)推荐25+束紧精华:优先改善皱纹(P=23.0)与光滑(P=57.2),其余维度维持现有护理节奏。整体肤质混合性,T区出油面积57%、左右脸颊20%/2%。',
|
||||
'综合文案仅产品判据按2026-10-12规则替换,其余逐字不变');
|
||||
ok(vm.lifestyle === '保持侧卧改仰卧的睡姿习惯,减少枕巾摩擦;控制屏幕时间,缓解眼周疲劳', '生活方式 = 最差分类皱纹条');
|
||||
}());
|
||||
|
||||
|
||||
+9
-1
@@ -157,5 +157,13 @@ module.exports = function createAccounts(root, env = process.env) {
|
||||
.concat(user.reports.filter(r => r.id !== snapshot.id)).slice(0, 5);
|
||||
save();
|
||||
}
|
||||
return { route, session, storeReport, sameOrigin, wechatAvailable: available };
|
||||
// Internal admin use only: reuse historical copy, never regenerate it.
|
||||
// Not a public route; validate the stored owner and report ID before reading.
|
||||
function reportPresentation(ownerId, id) {
|
||||
const user = db.users.find(u => u.id === ownerId);
|
||||
if (!user || !/^R[\w-]+$/.test(id)) return null;
|
||||
try { return JSON.parse(fs.readFileSync(path.join(snapshots, user.id, id + '.json'), 'utf8')).presentation || null; }
|
||||
catch (_) { return null; }
|
||||
}
|
||||
return { route, session, storeReport, reportPresentation, sameOrigin, wechatAvailable: available };
|
||||
};
|
||||
|
||||
+22
-8
@@ -257,6 +257,9 @@ function fillSuccess(rec, fpJson) {
|
||||
rec.detect_status = '成功';
|
||||
rec.fail_stage = null; rec.fail_code = null; rec.fail_msg = null;
|
||||
rec.skin_age = vm.skinAgeRaw;
|
||||
rec.skin_age_display = vm.skinAgeDisplay;
|
||||
rec.recommendation = vm.recommendation;
|
||||
rec.presentation = ReportUI.build(vm, fpJson.result);
|
||||
rec.total_score = vm.total;
|
||||
rec.jawline_angle_l = jawlineAngle(fpJson.left_side_result, 'left');
|
||||
rec.jawline_angle_r = jawlineAngle(fpJson.right_side_result, 'right');
|
||||
@@ -433,7 +436,7 @@ async function handleAnalyze(req, res) {
|
||||
let reportSaved = false, presentation = null, snapshotVm = null;
|
||||
try {
|
||||
snapshotVm = SkinScore.buildViewModel(result, payload.side, scoreProfile(rec));
|
||||
presentation = ReportUI.build(snapshotVm, result);
|
||||
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) });
|
||||
@@ -482,6 +485,12 @@ function photoUrls(rec) {
|
||||
}
|
||||
|
||||
/* 列表用的字段子集(不含 raw_json 与文案全文) */
|
||||
function displaySkinAge(rec) {
|
||||
if (Object.prototype.hasOwnProperty.call(rec, 'skin_age_display')) return rec.skin_age_display;
|
||||
// 旧记录没有展示值,沿用生成时的规则,不重算推荐或改写文件。
|
||||
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) {
|
||||
return {
|
||||
record_id: rec.record_id,
|
||||
@@ -490,6 +499,7 @@ function listItem(rec) {
|
||||
shoot_status: rec.shoot_status, upload_status: rec.upload_status, detect_status: rec.detect_status,
|
||||
fail_code: rec.fail_code, fail_msg: rec.fail_msg,
|
||||
total_score: rec.total_score, skin_age: rec.skin_age,
|
||||
skin_age_display: displaySkinAge(rec), recommendation: rec.recommendation || null,
|
||||
dims: rec.dims, group: rec.group,
|
||||
essence: rec.essence ? rec.essence.name : null,
|
||||
cost_ms: rec.cost_ms,
|
||||
@@ -576,7 +586,8 @@ function handleRecordDetail(req, res, id) {
|
||||
.map(r => ({ record_id: r.record_id, detect_status: r.detect_status, created_at: r.created_at }));
|
||||
sendJson(res, 200, {
|
||||
ok: true,
|
||||
record: rec,
|
||||
record: Object.assign({}, rec, { skin_age_display: displaySkinAge(rec),
|
||||
presentation: rec.presentation || accounts.reportPresentation(rec.owner_id, rec.record_id) }),
|
||||
raw_json: rawJson,
|
||||
photos: photoUrls(rec),
|
||||
versions: {
|
||||
@@ -733,21 +744,24 @@ function handleExport(req, res, url) {
|
||||
.sort((a, b) => (a.created_at < b.created_at ? 1 : -1));
|
||||
const dimP = (r, d) => (r.dims && r.dims[d]) ? r.dims[d].P : null;
|
||||
const dimS = (r, d) => (r.dims && r.dims[d]) ? r.dims[d].S : null;
|
||||
const dimLv = (r, d) => (r.dims && r.dims[d]) ? r.dims[d].level : null;
|
||||
const dimLv = (r, d) => (r.dims && r.dims[d]) ? ReportUI.levelMap[r.dims[d].level] : null;
|
||||
const header = ['记录ID', '创建时间', '年龄', '性别', '地区', '日常肤质', '拍摄状态', '上传状态', '检测状态',
|
||||
'失败环节', '失败码', '失败信息', '总分', '肌肤年龄',
|
||||
'紧塑力P', '紧塑力S', '紧塑力档', '活力感P', '活力感S', '活力感档', '稳定力P', '稳定力S', '稳定力档',
|
||||
'均分S', '产品组别', '推荐精华', '触发判据', '耗时ms'];
|
||||
'失败环节', '失败码', '失败信息', '总分', '原始肌龄(推荐依据)', '前端展示肌龄',
|
||||
'紧塑力P', '紧塑力S', '紧塑力档', '透亮感P', '透亮感S', '透亮感档', '稳韧力P', '稳韧力S', '稳韧力档',
|
||||
'均分S', '产品组别', '推荐精华', '触发判据', '耗时ms', '推荐规则版本', '推荐原因'];
|
||||
const lines = [header.join(',')];
|
||||
for (const r of filtered) {
|
||||
lines.push([
|
||||
r.record_id, r.created_at, r.real_age, r.gender, r.region, r.daily_skin_type,
|
||||
r.shoot_status, r.upload_status, r.detect_status,
|
||||
r.fail_stage, r.fail_code, r.fail_msg, r.total_score, r.skin_age,
|
||||
r.fail_stage, r.fail_code, r.fail_msg, r.total_score, r.skin_age, displaySkinAge(r),
|
||||
dimP(r, 'firm'), dimS(r, 'firm'), dimLv(r, 'firm'),
|
||||
dimP(r, 'vita'), dimS(r, 'vita'), dimLv(r, 'vita'),
|
||||
dimP(r, 'stab'), dimS(r, 'stab'), dimLv(r, 'stab'),
|
||||
r.avg_s, r.group, r.essence ? r.essence.name : null, r.trigger, r.cost_ms
|
||||
r.avg_s, r.group, r.essence ? ((ReportUI.products.find(p => p.match === r.essence.name) || {}).title || r.essence.name) : null,
|
||||
r.trigger, r.cost_ms,
|
||||
r.recommendation ? r.recommendation.version : '旧版规则(历史记录)',
|
||||
r.recommendation ? r.recommendation.reason : null
|
||||
].map(csvCell).join(','));
|
||||
}
|
||||
const fromTag = (q.from || 'all').replace(/[^\d-]/g, '');
|
||||
|
||||
@@ -37,6 +37,10 @@ const ReportUI = require('../js/report-ui');
|
||||
payload: { result: { ...SkinScore.MOCK_RESULT, face_maps: { brown_area: 'data:image/png;base64,saved-layer' } }, side: SkinScore.MOCK_SIDE } };
|
||||
accounts.storeReport(a.data.user.id, snapshot);
|
||||
assert.deepEqual((await request('/api/reports/Rtest0', null, a.cookie)).data.report, JSON.parse(JSON.stringify(snapshot)));
|
||||
assert.deepEqual(accounts.reportPresentation(a.data.user.id, 'Rtest0'), JSON.parse(JSON.stringify(presentation)));
|
||||
assert.equal(accounts.reportPresentation(b.data.user.id, 'Rtest0'), null);
|
||||
assert.equal(accounts.reportPresentation(a.data.user.id, '../identities'), null);
|
||||
assert.equal(accounts.reportPresentation('../reports', 'Rtest0'), null);
|
||||
assert.equal((await request('/api/reports/Rtest0', null, b.cookie)).status, 404);
|
||||
assert.equal((await request('/api/reports', null, b.cookie)).data.total, 0);
|
||||
for (let i = 1; i <= 5; i++) accounts.storeReport(a.data.user.id, { ...snapshot, id: 'Rtest' + i });
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
'use strict';
|
||||
const assert = require('node:assert/strict');
|
||||
const SS = require('../js/skin-score.js');
|
||||
const UI = require('../js/report-ui.js');
|
||||
let count = 0;
|
||||
function check(age, firm, vita, stab, tier, adjustment = 'none') {
|
||||
const r = SS.recommendProduct(age, { firm, vita, stab });
|
||||
assert.equal(r.tier, tier, `age=${age}; scores=${firm}/${vita}/${stab}`);
|
||||
assert.equal(r.adjustment, adjustment);
|
||||
assert.equal(r.version, '2026-10-12-skin-age-v1');
|
||||
if (tier) assert(Math.abs(r.tier - r.baseTier) <= 1);
|
||||
count++;
|
||||
}
|
||||
// Explicit decision-table expectations, including fractional ages and inclusive endpoints.
|
||||
for (const [age, normal, low, high] of [
|
||||
[24, 1, 1, 1], [31.99, 1, 1, 1], [32, 1, 2, 1], [34.99, 1, 2, 1],
|
||||
[35, 2, 2, 1], [36, 2, 2, 1], [37, 2, 2, 1], [38, 2, 2, 1], [38.01, 2, 2, 2],
|
||||
[46.99, 2, 2, 2], [47, 2, 3, 2], [49.99, 2, 3, 2],
|
||||
[50, 3, 3, 2], [51, 3, 3, 2], [52, 3, 3, 2], [53, 3, 3, 2], [53.01, 3, 3, 3], [80, 3, 3, 3]
|
||||
]) {
|
||||
check(age, 8, 8, 8, normal);
|
||||
check(age, 7.99, 10, 10, low, low > normal ? 'up' : 'none');
|
||||
check(age, 9, 9, 7, high, high < normal ? 'down' : 'none');
|
||||
check(age, 9, 7, 9, high, high < normal ? 'down' : 'none');
|
||||
}
|
||||
// Thresholds are compared before display rounding, not to 1- or 2-decimal scores.
|
||||
for (const age of [32, 34.99, 47, 49.99]) {
|
||||
const base = age < 35 ? 1 : 2;
|
||||
check(age, 7.99999, 10, 10, base + 1, 'up');
|
||||
check(age, 8, 10, 10, base);
|
||||
check(age, null, 10, 10, base);
|
||||
}
|
||||
for (const age of [35, 38, 50, 53]) {
|
||||
const base = age < 50 ? 2 : 3;
|
||||
check(age, 8.99999, 10, 10, base);
|
||||
check(age, 9, 8.99999, 8.99999, base);
|
||||
check(age, 9, null, null, base);
|
||||
check(age, null, 10, 10, base);
|
||||
check(age, 9, null, 9, base - 1, 'down');
|
||||
check(age, 9, 9, null, base - 1, 'down');
|
||||
}
|
||||
check(37, 9, 7, 9, 1, 'down'); // Only dullness: the third dimension may be arbitrarily low.
|
||||
check(35, 8.99, 10, 10, 2); // Firmness is necessary, other two alone do not downgrade.
|
||||
check(24, null, null, null, 1);
|
||||
check(40, null, null, null, 2);
|
||||
for (const invalid of [null, undefined, NaN, Infinity, -Infinity, 0, -5, '37', {}]) {
|
||||
check(invalid, 10, 10, 10, null, 'unavailable');
|
||||
}
|
||||
const raw = JSON.parse(JSON.stringify(SS.MOCK_RESULT));
|
||||
raw.skin_age = { value: 24 };
|
||||
for (const realAge of [24, 40, 60, undefined]) {
|
||||
const vm = SS.buildViewModel(raw, SS.MOCK_SIDE, { realAge });
|
||||
assert.equal(vm.essence.tier, 1); // Weak wrinkles never upgrade under32.
|
||||
assert.equal(vm.recommendation.skinAge, 24);
|
||||
assert(Math.abs(vm.recommendation.scores.firm - 7.69) < 1e-12);
|
||||
assert.equal(UI.build(vm, raw).product.tier, 1);
|
||||
if (realAge === 60) assert.equal(vm.skinAgeDisplay, 57); // Display isn't the recommendation input.
|
||||
count++;
|
||||
}
|
||||
delete raw.skin_age;
|
||||
const missing = SS.buildViewModel(raw, SS.MOCK_SIDE, { realAge: 60 });
|
||||
assert.equal(missing.group, null);
|
||||
assert.equal(missing.essence.tier, null);
|
||||
assert.equal(UI.build(missing, raw).product, null);
|
||||
assert(missing.recommendation.reason.includes('未获取有效原始肌龄'));
|
||||
count++;
|
||||
console.log(`PASS: ${count} recommendation cases; boundary/threshold/missing/raw-age/actual-age isolation`);
|
||||
+12
-4
@@ -8,7 +8,8 @@
|
||||
* - 6 分类文案(L/P/T/X/AB/AF)、综合文案(AW)、生活方式(AX) 逐字一致
|
||||
* - 浮点容差 0.05;文案逐字无容差
|
||||
*
|
||||
* 引擎与真值不一致时:不改 h5/js/skin-score.js(审计代理负责),差异登记在 KNOWN_DIFFS
|
||||
* 产品推荐按2026-10-12用户确认规则覆盖旧Excel期望;评分真值和原始fixture不改写。
|
||||
* 非授权的新差异登记在 KNOWN_DIFFS
|
||||
* 并输出明细;未登记的新差异 → 退出码 1。登记项若转为通过(引擎已修复)会提示清理。
|
||||
*/
|
||||
'use strict';
|
||||
@@ -20,6 +21,12 @@ var SkinScore = require(path.join(__dirname, '..', 'js', 'skin-score.js'));
|
||||
/* ─── 已知差异登记:键 = 案例.列,值 = 差异原因(逐条说明)。当前为空:V2.2 审计后
|
||||
引擎与两组真值全部一致;今后出现引擎/真值不一致时在此登记,未登记的新差异 → 退出码 1 ─── */
|
||||
var KNOWN_DIFFS = {};
|
||||
var RECOMMENDATION_EXPECT = {
|
||||
case1: { essence: '25+束紧精华', oldSummary: '按三重判据(触发:最差项约束)推荐35+支撑精华',
|
||||
newSummary: '按肤龄及边界规则(触发:肤龄基础分组)推荐25+束紧精华' },
|
||||
case2: { essence: '50+重塑精华', oldSummary: '按三重判据(触发:年龄组档)推荐50+重塑精华',
|
||||
newSummary: '按肤龄及边界规则(触发:肤龄基础分组)推荐50+重塑精华' }
|
||||
};
|
||||
|
||||
/* ─── V2.3 §8.8 护理建议成品真值(五段,逐字) ─── */
|
||||
var V23_SKIN = { case1: '混合偏干肌肤', case2: '干性肌肤' };
|
||||
@@ -28,7 +35,7 @@ var V23_EXPECT = {
|
||||
{ tag: '肤质', text: '你的日常肤质为混合偏干肌肤:T区状态尚可,但两颊及面部外侧偏干,易出现紧绷与细纹。建议分区护理:两颊侧重保湿滋养,选用滋润型乳液/面霜,T区轻量保湿即可,避免全脸统一用厚重面霜。干燥会放大纹理粗糙感,建议先将两颊水合度提上来再做角质调理。' },
|
||||
{ tag: '皱纹', text: '加强纹路分级管理,重点针对眼周、法令纹等动态纹高发区域,使用视黄醇/胜肽类淡纹精华并配合点按手法,遏制静态纹转化。' },
|
||||
{ tag: '光滑', text: '加强细腻度管理,避免过度清洁破坏角质屏障,重点关注毛孔显著区域,配合低浓度酸类(如水杨酸)周期护理与柔润精华。' },
|
||||
{ tag: '精华', text: '建议搭配使用35+支撑精华:专为轻熟肌打造,提升弹韧表现、维持面部轮廓清晰度,改善疲态并延缓衰老速度。' },
|
||||
{ tag: '精华', text: '建议搭配使用25+束紧精华:针对年轻肌肤活力减弱问题,激活肌肤动力、改善暗沉,提升弹润紧致表现,守护年轻光泽并延缓初始衰老迹象。' },
|
||||
{ tag: '生活方式', text: '保持侧卧改仰卧的睡姿习惯,减少枕巾摩擦;控制屏幕时间,缓解眼周疲劳' }
|
||||
],
|
||||
case2: [
|
||||
@@ -120,9 +127,10 @@ function main() {
|
||||
}
|
||||
// 组别/精华/均分/综合文案/生活方式
|
||||
okStr(tag + '.AT 产品组别', String(vm.group), tr.group);
|
||||
okStr(tag + '.AU 精华产品线', vm.essence.name, tr.essence);
|
||||
okStr(tag + '.AU 精华产品线(肤龄新版)', vm.essence.name, RECOMMENDATION_EXPECT[tag].essence);
|
||||
okNum(tag + '.AV 六分类S均分', vm.avgS, tr.avgS);
|
||||
okStr(tag + '.AW 综合文案', vm.summary, tr.summary, tag + '.AW');
|
||||
okStr(tag + '.AW 综合文案', vm.summary, tr.summary.replace(RECOMMENDATION_EXPECT[tag].oldSummary,
|
||||
RECOMMENDATION_EXPECT[tag].newSummary), tag + '.AW');
|
||||
okStr(tag + '.AX 生活方式', vm.lifestyle, tr.lifestyle);
|
||||
|
||||
// V2.3 §8 护理建议五段(真值:§8.8 样例;daily_skin_type 按样例取,与 summary 口径的肤质参数不同)
|
||||
|
||||
@@ -8,6 +8,9 @@ import http from 'node:http';
|
||||
import { spawn, execSync } from 'node:child_process';
|
||||
import { once } from 'node:events';
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||
import { createRequire } from 'node:module';
|
||||
const require = createRequire(import.meta.url);
|
||||
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-'));
|
||||
@@ -40,15 +43,68 @@ try {
|
||||
await page.click('[data-action="login-browser"]');
|
||||
await page.waitForSelector('#s-splash.active');
|
||||
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: 28, daily_skin_type: '干性肌肤' } } })).json();
|
||||
const result = await (await api.post(base + 'api/skin-analyze', { data: { front_image: fixture.photos[0], left_image: fixture.photos[0], right_image: fixture.photos[0], profile: { real_age: 60, daily_skin_type: '干性肌肤' } } })).json();
|
||||
assert.equal(result.ok, true);
|
||||
assert.equal(result.report_saved, true);
|
||||
assert(result.vm && result.presentation);
|
||||
assert(result.vm.skinAgeRaw < 32);
|
||||
assert.equal(result.vm.skinAgeDisplay, 57);
|
||||
assert.equal(result.vm.essence.tier, 1); // Raw age, not actual60 or display57.
|
||||
assert.equal(result.presentation.product.tier, 1);
|
||||
const adminRecord = (await (await api.get(base + 'api/admin/records/' + result.record_id)).json()).record;
|
||||
assert.deepEqual(adminRecord.presentation, result.presentation);
|
||||
assert.deepEqual(adminRecord.recommendation, result.vm.recommendation);
|
||||
assert.equal(adminRecord.skin_age, result.vm.skinAgeRaw);
|
||||
assert.equal(adminRecord.skin_age_display, result.vm.skinAgeDisplay);
|
||||
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();
|
||||
assert.equal(list.items[0].skin_age, result.vm.skinAgeRaw);
|
||||
assert.equal(list.items[0].skin_age_display, 57);
|
||||
assert.deepEqual(list.items[0].recommendation, result.vm.recommendation);
|
||||
const csv = await (await api.get(base + 'api/admin/export')).text();
|
||||
assert(csv.includes('原始肌龄(推荐依据),前端展示肌龄'));
|
||||
assert(csv.includes(result.vm.recommendation.version));
|
||||
const snapshot = await (await api.get(base + 'api/reports/' + result.record_id)).json();
|
||||
assert.equal(snapshot.report.photos.length, 3);
|
||||
assert.deepEqual(snapshot.report.presentation, result.presentation);
|
||||
assert.deepEqual(snapshot.report.payload.result.face_maps, result.result.face_maps);
|
||||
assert.equal(snapshot.report.photos[0], fixture.photos[0]);
|
||||
const desktop = await browser.newPage({ viewport: { width: 1600, height: 1000 } });
|
||||
desktop.on('pageerror', e => errors.push(e.message));
|
||||
await desktop.goto(base + 'admin.html');
|
||||
const row = desktop.locator('#tbl-records tbody tr').first();
|
||||
await row.waitFor();
|
||||
assert((await row.textContent()).includes('原始 ' + result.vm.skinAgeRaw + '岁'));
|
||||
assert((await row.textContent()).includes('展示 57岁'));
|
||||
assert((await row.textContent()).includes("1'me 主场精华 I(25+)"));
|
||||
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.deepEqual(await desktop.locator('#f-dimlevel option').allTextContents(), ['不限档位', '改善', '良好', '优秀']);
|
||||
if (process.env.H5_ARTIFACT_DIR) {
|
||||
fs.mkdirSync(process.env.H5_ARTIFACT_DIR, { recursive: true });
|
||||
await desktop.screenshot({ path: path.join(process.env.H5_ARTIFACT_DIR, 'admin-list.png'), fullPage: true });
|
||||
}
|
||||
await desktop.selectOption('#f-dim', 'firm');
|
||||
await desktop.selectOption('#f-dimlevel', adminRecord.dims.firm.level);
|
||||
const filtered = desktop.waitForResponse(r => r.url().includes('/api/admin/records?'));
|
||||
await desktop.click('#btn-search');
|
||||
assert.equal((await (await filtered).json()).total, 1);
|
||||
await row.click();
|
||||
await desktop.waitForFunction(() => document.querySelector('#d-body').textContent.includes('推荐依据'));
|
||||
const detailText = await desktop.locator('#d-body').textContent();
|
||||
assert(detailText.includes(result.vm.recommendation.reason));
|
||||
assert(detailText.includes('前端展示肌龄 57岁'));
|
||||
for (const view of result.presentation.views) assert(detailText.includes(view.text));
|
||||
for (const care of result.presentation.care) assert(detailText.includes(care.text));
|
||||
if (process.env.H5_ARTIFACT_DIR) {
|
||||
await desktop.locator('.d-sec').filter({ hasText: '使用原始肌龄' }).scrollIntoViewIfNeeded();
|
||||
await desktop.screenshot({ path: path.join(process.env.H5_ARTIFACT_DIR, 'admin-recommendation.png') });
|
||||
}
|
||||
await desktop.click('#btn-close');
|
||||
await desktop.click('[data-tab="stats"]');
|
||||
await desktop.waitForFunction(() => document.querySelector('#dim-dist').textContent.includes('透亮感'));
|
||||
assert(!(await desktop.locator('#dim-dist').textContent()).includes('预警'));
|
||||
// A separate browser cannot access this user's report, even knowing its ID.
|
||||
const other = await browser.newContext();
|
||||
assert.equal((await other.request.get(base + 'api/reports/' + result.record_id)).status(), 401);
|
||||
@@ -78,12 +134,55 @@ try {
|
||||
await page.waitForFunction(() => [...document.querySelectorAll('.ditem-img')].every(i => !i.hidden && i.naturalWidth));
|
||||
assert.deepEqual(await page.locator('.ditem-img').evaluateAll(images => images.map(i => i.src)), imagesBefore);
|
||||
assert.deepEqual(await page.locator('.ditem-para').allTextContents(), copyBefore);
|
||||
assert.equal(await page.locator('#product-title').textContent(), result.presentation.product.title);
|
||||
await page.goBack();
|
||||
await page.waitForSelector('#s-history.active');
|
||||
await page.goBack();
|
||||
await page.waitForSelector('#s-splash.active');
|
||||
// Simulate an old, immutable report whose original product was II, even though
|
||||
// today's rule would choose I. Admin must reuse its saved frontend copy, too.
|
||||
const recordFile = path.join(dataRoot, 'records.jsonl');
|
||||
const record = JSON.parse(fs.readFileSync(recordFile, 'utf8').trim());
|
||||
const snapshotFile = path.join(dataRoot, 'accounts/reports', record.owner_id, result.record_id + '.json');
|
||||
const legacy = JSON.parse(fs.readFileSync(snapshotFile, 'utf8'));
|
||||
legacy.presentation.product = ReportUI.products[1];
|
||||
legacy.presentation.views[0].text = '历史报告原文,不应按新文案重新计算。';
|
||||
legacy.vm.essence = { name: '35+支撑精华', text: '旧版推荐', triggers: ['最差项约束'] };
|
||||
delete legacy.vm.recommendation;
|
||||
fs.writeFileSync(snapshotFile, JSON.stringify(legacy));
|
||||
record.essence = legacy.vm.essence;
|
||||
delete record.presentation;
|
||||
delete record.recommendation;
|
||||
delete record.skin_age_display;
|
||||
fs.writeFileSync(recordFile, JSON.stringify(record) + '\n');
|
||||
const storedBytes = fs.readFileSync(snapshotFile, 'utf8');
|
||||
const recordBytes = fs.readFileSync(recordFile, 'utf8');
|
||||
await page.click('[data-action="history"]');
|
||||
await page.click('[data-report-id]');
|
||||
await page.waitForSelector('#s-report.active');
|
||||
assert.equal(await page.locator('#product-title').textContent(), ReportUI.products[1].title);
|
||||
assert((await page.locator('.ditem-para').allTextContents()).includes(legacy.presentation.views[0].text));
|
||||
await desktop.goto(base + 'admin.html');
|
||||
await desktop.locator('#tbl-records tbody tr').first().click();
|
||||
await desktop.waitForFunction(() => document.querySelector('#d-body').textContent.includes('历史报告原文'));
|
||||
assert((await desktop.locator('#d-body').textContent()).includes('旧版规则(历史记录)'));
|
||||
assert.equal(fs.readFileSync(snapshotFile, 'utf8'), storedBytes);
|
||||
assert.equal(fs.readFileSync(recordFile, 'utf8'), recordBytes);
|
||||
// Missing skin age must not leave the previous product visible in the H5.
|
||||
delete legacy.payload.result.skin_age;
|
||||
const SkinScore = require('../js/skin-score.js');
|
||||
legacy.vm = SkinScore.buildViewModel(legacy.payload.result, legacy.payload.side, { realAge: 60 });
|
||||
legacy.presentation = ReportUI.build(legacy.vm, legacy.payload.result);
|
||||
fs.writeFileSync(snapshotFile, JSON.stringify(legacy));
|
||||
await page.reload();
|
||||
await page.waitForSelector('#s-splash.active');
|
||||
await page.click('[data-action="history"]');
|
||||
await page.click('[data-report-id]');
|
||||
await page.waitForSelector('#s-report.active');
|
||||
assert.equal(await page.locator('.card-product').isVisible(), false);
|
||||
assert.equal(await page.locator('#product-unavailable').isVisible(), true);
|
||||
assert.deepEqual(errors, []);
|
||||
console.log('PASS: authenticated analyze → full snapshot → backend history → reload → seven identical images/copy; owner isolation, private files, browser 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 {
|
||||
if (browser) await browser.close();
|
||||
child.kill('SIGTERM');
|
||||
|
||||
Reference in New Issue
Block a user