chore+perf: 密钥出库 + 后台详情接口裁剪 face_maps

安全:
- reference/facepp-skinanalyze/credentials.env 内含 Face++ 正式 key/secret
  且被 git 跟踪,移出 index(文件保留本地,.gitignore 已覆盖 *.env)。
  ⚠️ 密钥已存在于 git 历史,若 origin 曾被 push 即视为泄漏,
  请在旷视控制台重置 API secret,改用环境变量/apikey.bash 注入。

性能(后台详情页卡顿):
- 详情接口将 raw_json.result.face_maps 的 base64 图层(单条约 3MB)
  替换为占位说明,响应 3.2MB→163KB;新增
  GET /api/admin/records/:id/raw 供下载完整原始 JSON(走 Basic Auth)
- admin 列表/详情图片加 loading=lazy + decoding=async
This commit is contained in:
Ubuntu
2026-09-28 22:46:08 +08:00
parent 3de730a127
commit c06d15a2d4
3 changed files with 36 additions and 21 deletions
+7 -4
View File
@@ -115,7 +115,7 @@ function loadRecords() {
var tr = el('tr');
var photos = it.photos || {};
var thumb = photos.front
? '<img class="thumb" src="' + photos.front + '" alt="正脸">'
? '<img class="thumb" loading="lazy" decoding="async" src="' + photos.front + '" alt="正脸">'
: '<span class="sub">无</span>';
tr.innerHTML =
'<td>' + thumb + '</td>' +
@@ -321,7 +321,7 @@ function renderDetail(data) {
var u = data.photos[w];
if (!u) continue;
anyPhoto = true;
photosHtml += '<div class="d-photo"><img src="' + u + '" alt="' + whichNames[w] + '" data-zoom="' + u + '">' +
photosHtml += '<div class="d-photo"><img loading="lazy" decoding="async" src="' + u + '" alt="' + whichNames[w] + '" data-zoom="' + u + '">' +
'<a href="' + u + '" download="' + r.record_id + '-' + w + '.jpg">下载' + whichNames[w] + '</a></div>';
}
photosHtml += anyPhoto ? '' : '<span class="sub">无照片</span>';
@@ -429,9 +429,12 @@ function renderDetail(data) {
}
}
/* 接口原始 JSON:details 折叠,pre 展示 */
/* 接口原始 JSON:details 折叠,pre 展示(face_maps 已在服务端裁剪,完整版走下载链接) */
if (data.raw_json) {
h += dSec('接口原始 JSON', '<details><summary>展开/收起(raw_json,' + (data.raw_file || '') + ')</summary><pre>' +
var rawDl = data.raw_file
? ' <a href="/api/admin/records/' + encodeURIComponent(r.record_id) + '/raw" download>下载完整原始 JSON</a>'
: '';
h += dSec('接口原始 JSON', '<details><summary>展开/收起(' + (data.raw_file || '') + ')' + rawDl + '</summary><pre>' +
esc(JSON.stringify(data.raw_json, null, 1)) + '</pre></details>');
} else {
h += dSec('接口原始 JSON', '<div class="sub">无(失败记录或未落盘)</div>');
+29 -1
View File
@@ -524,13 +524,23 @@ function handleRecordsQuery(req, res, url) {
function handleRecordDetail(req, res, id) {
const rec = findRecord(id);
if (!rec) return sendJson(res, 404, { ok: false, message: '记录不存在' });
// 原始 JSON 全文:读文件失败给 null
// 原始 JSON 全文:读文件失败给 null。
// face_maps 是整幅 base64 叠加图(单文件可达数 MB),详情页直接渲染会卡死浏览器:
// 展示层把各图层值替换为占位说明,完整原文件走 /api/admin/records/:id/raw 下载
let rawJson = null;
if (rec.raw_file) {
const fname = String(rec.raw_file).replace(/[^\w.,-]/g, '');
try { rawJson = JSON.parse(fs.readFileSync(path.join(DATA_DIR, fname), 'utf8')); }
catch (e) { rawJson = null; }
}
if (rawJson && rawJson.result && rawJson.result.face_maps) {
for (const k of Object.keys(rawJson.result.face_maps)) {
const v = rawJson.result.face_maps[k];
if (typeof v === 'string' && v.length) {
rawJson.result.face_maps[k] = `<base64 图层已省略,共 ${v.length} 字符,点上方链接下载完整原文件>`;
}
}
}
// 版本链:它 retry_of 指向的(父)+ retry_of 指向它的(子)
const all = readAllRecords();
const parent = rec.retry_of ? (all.find(r => r.record_id === rec.retry_of) || null) : null;
@@ -548,6 +558,22 @@ function handleRecordDetail(req, res, id) {
});
}
/* 完整原始 JSON 下载(face_maps base64 未裁剪,动辄数 MB,仅供复核下载,不做页面内展示) */
function handleRawDownload(req, res, id) {
const rec = findRecord(id);
if (!rec || !rec.raw_file) return sendJson(res, 404, { ok: false, message: '记录或原始 JSON 不存在' });
const fname = String(rec.raw_file).replace(/[^\w.,-]/g, '');
const file = path.join(DATA_DIR, fname);
fs.readFile(file, (err, data) => {
if (err) return sendJson(res, 404, { ok: false, message: '原始 JSON 文件不存在' });
res.writeHead(200, {
'Content-Type': 'application/json; charset=utf-8',
'Content-Disposition': `attachment; filename="${rec.record_id}-raw.json"`
});
res.end(data);
});
}
function handlePhoto(req, res, id, which) {
const safe = String(id || '').replace(/[^\w-]/g, '');
const file = path.join(PHOTOS_DIR, safe, which + '.jpg');
@@ -710,6 +736,8 @@ async function handleAdmin(req, res, pathname, url) {
try {
const retryM = pathname.match(/^\/api\/admin\/records\/([\w-]+)\/retry$/);
if (req.method === 'POST' && retryM) return await handleRetry(req, res, retryM[1]);
const rawM = pathname.match(/^\/api\/admin\/records\/([\w-]+)\/raw$/);
if (req.method === 'GET' && rawM) return handleRawDownload(req, res, rawM[1]);
const detailM = pathname.match(/^\/api\/admin\/records\/([\w-]+)$/);
if (req.method === 'GET' && detailM) return handleRecordDetail(req, res, detailM[1]);
const photoM = pathname.match(/^\/api\/admin\/photos\/([\w-]+)\/(front|left|right)$/);
@@ -1,16 +0,0 @@
# ~/.openclaw/credentials.env — 统一凭据存放(勿提交 git、勿发到聊天)
# 用法: set -a; source ~/.openclaw/credentials.env; set +a
# ---- 旷视 Face++(巨子测肤 skinanalyze_pro 测试接口)----
# 试用key(cAa5...)已弃用: 旷视规则试用key不能调专业版。当前为正式key(EcZ...)
FACEPP_API_KEY=EcZiqYhA8eQSiRgXVem8mijgzPfSC2qp
FACEPP_API_SECRET=p62QRWv9tQitZ1tvXmEBV8AgmaS00lO8
# ---- FaceStyle 专业版调用参数 ----
FACEPP_SKINANALYZE_URL=https://api-cn.faceplusplus.com/facepp/v1/skinanalyze_pro
# ---- 飞书 / OCR(合同盖章核验项目)----
# 实际值在 ~/Desktop/合同盖章核验项目/service/.env(未入库)
# ---- 智谱 / 火山引擎 ----
# 已存于 ~/.zshrc 及 openclaw.json providers 配置