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