feat(rednote): add isolated persistent browser H5 entry
This commit is contained in:
+24
-12
@@ -20,8 +20,10 @@ const PORT = Number(process.env.PORT) || 8080;
|
||||
// 只绑回环:公网入口是 nginx(1me.xajuzi.com/state/),8080 不再对公网裸奔;需局域网调试时 HOST=0.0.0.0
|
||||
const HOST = process.env.HOST || '127.0.0.1';
|
||||
const ROOT = __dirname;
|
||||
const entry = require('./lib/entry-config')(process.env, ROOT);
|
||||
const providerFetch = entry.rednote ? require('./lib/provider-gate')((...args) => fetch(...args)) : (...args) => fetch(...args);
|
||||
const FACEPP_URL = 'https://api-cn.faceplusplus.com/facepp/v1/skinanalyze_pro';
|
||||
const DATA_ROOT = process.env.JUZI_DATA_ROOT ? path.resolve(process.env.JUZI_DATA_ROOT) : path.join(ROOT, 'data');
|
||||
const DATA_ROOT = entry.dataRoot;
|
||||
const DATA_DIR = path.join(DATA_ROOT, 'results'); // 每次检测的原始返回落盘(复核用,已在 .gitignore)
|
||||
const PHOTOS_DIR = path.join(DATA_ROOT, 'photos'); // 检测照片:data/photos/<record_id>/{front,left,right}.jpg
|
||||
const RECORDS_FILE = path.join(DATA_ROOT, 'records.jsonl'); // 检测记录(一行一条 JSON)
|
||||
@@ -34,7 +36,7 @@ const accounts = require('./lib/accounts.js')(path.join(DATA_ROOT, 'accounts'));
|
||||
const wrinkleJobs = require('./lib/wrinkle-jobs.js')({
|
||||
dir:path.join(DATA_ROOT,'wrinkle-jobs'),
|
||||
collect:async context=>WrinkleViews.forReport(await WrinkleViews.collect(context.primary,context.photos,{
|
||||
key:CRED.key,secret:CRED.secret,url:FACEPP_URL,gapMs:process.env.NODE_ENV==='test'?0:4000,deadline:Date.now()+60000
|
||||
key:CRED.key,secret:CRED.secret,url:FACEPP_URL,fetch:providerFetch,gapMs:process.env.NODE_ENV==='test'?0:4000,deadline:Date.now()+60000
|
||||
})),
|
||||
load(owner,id) {
|
||||
const snapshot=accounts.reportSnapshot(owner,id);
|
||||
@@ -116,6 +118,9 @@ function serveStatic(req, res, pathname) {
|
||||
if (file === DATA_ROOT || file.startsWith(DATA_ROOT + path.sep)) { res.writeHead(403); return res.end('Forbidden'); }
|
||||
fs.readFile(file, (err, data) => {
|
||||
if (err) { res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' }); return res.end('Not Found'); }
|
||||
if (entry.rednote && ['index.html', 'admin.html'].some(name => file === path.join(ROOT, name))) {
|
||||
data = Buffer.from(require('./lib/rednote-view')(data.toString('utf8'), path.basename(file) === 'admin.html'));
|
||||
}
|
||||
const ext = path.extname(file);
|
||||
// Revalidate every time: updates remain immediate; unchanged resources use
|
||||
// 304 instead of downloading again. APIs/private reports never enter here.
|
||||
@@ -360,7 +365,7 @@ async function runDetect(rec, front, left, right, persist = true, deferWrinkles
|
||||
const fpJson = await callFacepp(front, left, right);
|
||||
if (fpJson.result && !fpJson.error_message && !deferWrinkles) {
|
||||
fpJson.wrinkle_views = await WrinkleViews.collect(fpJson,{left,right},{
|
||||
key:CRED.key,secret:CRED.secret,url:FACEPP_URL,
|
||||
key:CRED.key,secret:CRED.secret,url:FACEPP_URL,fetch:providerFetch,
|
||||
gapMs:process.env.NODE_ENV==='test'?0:4000,
|
||||
// Legacy admin retries may still collect inline; customer reports use
|
||||
// deferred tasks instead. Missing side maps never fail a report.
|
||||
@@ -404,7 +409,7 @@ async function callFacepp(front, left, right) {
|
||||
for (let attempt = 0; attempt < 2; attempt++) {
|
||||
fetchErr = null; fpRes = null; fpJson = null;
|
||||
try {
|
||||
fpRes = await fetch(FACEPP_URL, { method: 'POST', body: form, signal: AbortSignal.timeout(60000) });
|
||||
fpRes = await providerFetch(FACEPP_URL, { method: 'POST', body: form, signal: AbortSignal.timeout(60000) });
|
||||
fpJson = await fpRes.json().catch(() => ({}));
|
||||
} catch (e) { // 60s 超时(AbortError)/网络异常:无 fpJson,与 5xx/并发超限统一走「等 4s 重试 1 次」
|
||||
fetchErr = e;
|
||||
@@ -432,8 +437,8 @@ async function handleAnalyze(req, res, miniProgram = false, preparedBody = null,
|
||||
if (!accounts.sameOrigin(req)) return sendJson(res, 403, { ok: false, message: '请求来源不匹配' });
|
||||
const body = preparedBody || JSON.parse((await readBody(req, 20 * 1024 * 1024)).toString('utf8'));
|
||||
// A client-supplied channel must never change H5 guest retention behavior.
|
||||
const channel = miniProgram ? 'taobao' : 'h5';
|
||||
persist = ['wechat', 'taobao'].includes(owner.kind);
|
||||
const channel = miniProgram ? 'taobao' : entry.channel;
|
||||
persist = ['wechat', 'taobao', 'browser_persistent'].includes(owner.kind);
|
||||
// 三个 image 字段先做类型检查:非字符串视为缺失(左/右图缺失时仅发正脸,接口自动降级为单视图)
|
||||
const front = stripDataPrefix(typeof body.front_image === 'string' ? body.front_image : '');
|
||||
const leftB64 = stripDataPrefix(typeof body.left_image === 'string' ? body.left_image : '');
|
||||
@@ -572,9 +577,10 @@ function sendJson(res, status, obj) {
|
||||
/* 记录里照片的在线 URL(有存才给) */
|
||||
function photoUrls(rec) {
|
||||
const urls = {};
|
||||
if (rec.img_front) urls.front = `/api/admin/photos/${rec.record_id}/front`;
|
||||
if (rec.img_left) urls.left = `/api/admin/photos/${rec.record_id}/left`;
|
||||
if (rec.img_right) urls.right = `/api/admin/photos/${rec.record_id}/right`;
|
||||
const prefix = entry.rednote ? '/rednote' : '';
|
||||
if (rec.img_front) urls.front = `${prefix}/api/admin/photos/${rec.record_id}/front`;
|
||||
if (rec.img_left) urls.left = `${prefix}/api/admin/photos/${rec.record_id}/left`;
|
||||
if (rec.img_right) urls.right = `${prefix}/api/admin/photos/${rec.record_id}/right`;
|
||||
return urls;
|
||||
}
|
||||
|
||||
@@ -941,9 +947,10 @@ async function handleAdmin(req, res, pathname, url) {
|
||||
}
|
||||
}
|
||||
|
||||
let rednoteAnalyses = 0;
|
||||
http.createServer(async (req, res) => {
|
||||
const pathname = req.url.split('?')[0];
|
||||
if (pathname.startsWith('/api/taobao/')) return handleTaobao(req, res, pathname);
|
||||
if (pathname.startsWith('/api/taobao/')) return entry.rednote ? sendJson(res, 404, { ok: false }) : handleTaobao(req, res, pathname);
|
||||
if (await accounts.route(req, res, pathname)) return;
|
||||
if (req.method==='POST' && pathname==='/api/wrinkle-views') {
|
||||
const owner=accounts.session(req);
|
||||
@@ -962,10 +969,15 @@ http.createServer(async (req, res) => {
|
||||
return sendJson(res, accepted ? 200 : 400, { ok: accepted });
|
||||
} catch (e) { return sendJson(res, e.message === 'TOO_LARGE' ? 413 : e instanceof SyntaxError ? 400 : 500, { ok: false }); }
|
||||
}
|
||||
if (req.method === 'POST' && pathname === '/api/skin-analyze') return handleAnalyze(req, res);
|
||||
if (req.method === 'POST' && pathname === '/api/skin-analyze') {
|
||||
if (!entry.rednote) return handleAnalyze(req, res);
|
||||
if (rednoteAnalyses) return sendJson(res, 429, { ok: false, code: 'BUSY', message: '当前检测人数较多,请稍后重试' });
|
||||
rednoteAnalyses++;
|
||||
try { return await handleAnalyze(req, res); } finally { rednoteAnalyses--; }
|
||||
}
|
||||
if (req.method === 'GET' && pathname === '/api/health') {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
return res.end(JSON.stringify({ ok: true }));
|
||||
return res.end(JSON.stringify(entry.rednote ? { ok: true, channel: entry.channel } : { ok: true }));
|
||||
}
|
||||
if (pathname.startsWith('/api/admin/')) {
|
||||
return handleAdmin(req, res, pathname, new URL(req.url, 'http://localhost'));
|
||||
|
||||
Reference in New Issue
Block a user