Files
juzi/taobao/pages/camera/camera.js
T
xsl 774376fa16 feat: 完善淘宝小程序拍照、报告与登录流程
接入本地实时识别和自动三连拍,调整报告裁切、商品跳转、分享标题及居中页眉;完善游客会话、后台渠道隔离、部署脚本与回归测试。
2026-11-04 15:07:51 +08:00

947 lines
65 KiB
JavaScript

var api = require('../../utils/api'), image = require('../../utils/image'), voice = require('../../utils/voice');
var permissions = require('../../utils/permissions');
var development = require('../../utils/development');
var config = require('../../config');
var arCamera = require('../../utils/ar-camera');
var nativeQuality = require('../../utils/native-face-quality');
var CAPTURE_PROBE_VERSION = 'LOCAL-FACE-20261102-09';
var GPU_STAGES = ['uninitialized', 'create_canvas', 'get_context', 'native_preflight', 'register_backend', 'matmul', 'conv2d', 'readback', 'warmup', 'ready', 'context_lost', 'disposed'];
var GPU_CODES = ['NONE', 'API_MISSING', 'CANVAS_FAILED', 'CONTEXT_FAILED', 'INSTRUMENTATION_FAILED', 'PREFLIGHT_FAILED', 'DRAW_UNSUPPORTED', 'BACKEND_FAILED', 'MATMUL_FAILED', 'CONV_FAILED', 'READBACK_FAILED', 'CONTEXT_LOST', 'WARMUP_FAILED', 'DISPOSED'];
var GPU_SYNTHETIC_FIELDS = ['actualLength', 'nonFiniteCount', 'mismatchCount', 'maxAbsError', 'firstExpected', 'firstActual', 'expectedChecksum', 'actualChecksum'];
var NATIVE_GPU_STAGES = ['not_run', 'clear', 'constant_arrays', 'constant_indexed', 'constant_quad', 'shader', 'byte_texture', 'float_texture', 'passed'];
var NATIVE_GPU_CODES = ['NONE', 'READ_UNCHANGED', 'VALUE_MISMATCH', 'GL_ERROR', 'FRAMEBUFFER_INCOMPLETE', 'SHADER_COMPILE_FAILED', 'PROGRAM_LINK_FAILED', 'API_MISSING', 'API_FAILED'];
function nativeFrameSize(value) { return ['small', 'medium', 'large'].indexOf(value) >= 0 ? value : 'medium'; }
function nativeEngineOptions() {
var input = config.nativeFaceInputSize, side = config.nativeFaceMaxSide;
return { backend: config.nativeFaceBackend === 'webgl' ? 'webgl' : 'cpu', platform: my, defaultPixelFormat: config.nativePixelFormat || 'RGBA',
inputSize: typeof input === 'number' && input >= 128 && input <= 320 && input % 32 === 0 ? input : 256,
maxSide: typeof side === 'number' && side >= 128 && side <= 320 && Math.floor(side) === side ? side : 320,
landmarkModel: config.nativeFaceLandmarkModel === 'tiny' ? 'tiny' : 'full', scoreThreshold: nativeQuality.scoreThreshold('front') };
}
function probeValue(object, key) {
try { var descriptor = object && Object.getOwnPropertyDescriptor(object, key); return descriptor && Object.prototype.hasOwnProperty.call(descriptor, 'value') ? descriptor.value : undefined; } catch (_) {}
}
function probeNumber(value) { return typeof value === 'number' && isFinite(value) && value >= 0 ? value : 0; }
function probeEngineMetadata(info, previous) {
previous = previous || {};
var backend = probeValue(info, 'backend'), model = probeValue(info, 'landmarkModel'), optimization = probeValue(info, 'cpuOptimization');
function size(key) { var value = probeValue(info, key); return typeof value === 'number' && isFinite(value) && value > 0 && Math.floor(value) === value ? value : previous[key] || 'not_reported'; }
return { backend: ['cpu', 'webgl', 'wasm'].indexOf(backend) >= 0 ? backend : previous.backend || 'not_reported',
inputSize: size('inputSize'), maxSide: size('maxSide'), landmarkModel: ['tiny', 'full'].indexOf(model) >= 0 ? model : previous.landmarkModel || 'not_reported',
cpuOptimization: optimization === undefined ? previous.cpuOptimization || 'not_reported' : ['pad-copy', 'none'].indexOf(optimization) >= 0 ? optimization : 'not_reported' };
}
function probeTimingMetadata(info) {
var timings = probeValue(info, 'timings'), safe = {};
['snapshotMs', 'modelMs', 'tensorMs', 'detectorMs', 'landmarkMs', 'totalMs'].forEach(function (key) {
var value = probeValue(timings, key); safe[key] = typeof value === 'number' && isFinite(value) && value >= 0 ? value : 'not_reported';
});
return safe;
}
function probeSyntheticMetadata(info, previous) {
var safe = {};
GPU_SYNTHETIC_FIELDS.forEach(function (key) {
var value = probeValue(info, key);
if (value === undefined && previous) value = previous[key];
var valid = typeof value === 'number' && isFinite(value);
if (['actualLength', 'nonFiniteCount', 'mismatchCount'].indexOf(key) >= 0) valid = valid && value >= 0 && Math.floor(value) === value;
if (key === 'maxAbsError') valid = valid && value >= 0;
safe[key] = value === null ? null : valid ? value : 'not_reported';
});
return safe;
}
function probeNativeGpuMetadata(info, previous) {
previous = previous || {};
var safe = {};
[['stage', NATIVE_GPU_STAGES], ['code', NATIVE_GPU_CODES],
['readReturn', ['not_run', 'undefined', 'arraybuffer', 'typedarray', 'promise', 'other']],
['clear', ['not_run', 'passed']], ['constantArrays', ['not_run', 'passed']], ['constantIndexed', ['not_run', 'passed']], ['constantQuad', ['not_run', 'passed']],
['shader', ['not_run', 'passed']], ['byteTexture', ['not_run', 'passed']], ['floatTexture', ['not_run', 'passed']]
].forEach(function (entry) {
var value = probeValue(info, entry[0]);
if (value === undefined) value = previous[entry[0]];
safe[entry[0]] = entry[1].indexOf(value) >= 0 ? value : 'not_reported';
});
['changedBytes', 'mismatchCount', 'expectedChecksum', 'actualChecksum', 'glError', 'framebufferStatus', 'vertexBufferBytes', 'indexBufferBytes', 'pixel0', 'pixel1', 'pixel2', 'pixel3'].forEach(function (key) {
var value = probeValue(info, key);
if (value === undefined) value = previous[key];
var valid = typeof value === 'number' && isFinite(value) && value >= 0 && Math.floor(value) === value;
if (/^pixel[0-3]$/.test(key)) valid = valid && value <= 4294967295;
safe[key] = valid ? value : 'not_reported';
});
return safe;
}
function probeGpuMetadata(info, previous, includeSynthetic) {
previous = previous || {};
var safe = {}, requested = probeValue(info, 'requested'), lost = probeValue(info, 'contextLost');
safe.requested = typeof requested === 'boolean' ? requested : previous.requested === true;
safe.contextLost = typeof lost === 'boolean' ? lost : previous.contextLost === true;
[['selftest', ['not_run', 'running', 'passed', 'failed']], ['stage', GPU_STAGES], ['code', GPU_CODES]].forEach(function (entry) {
var value = probeValue(info, entry[0]);
safe[entry[0]] = entry[1].indexOf(value) >= 0 ? value : value === undefined && previous[entry[0]] ? previous[entry[0]] : 'not_reported';
});
// These values belong exclusively to fixed, synthetic startup selftests.
// Never refresh them from a model detection, camera frame or later stats.
['matmulDraws', 'convDraws', 'pixelReads', 'floatReads', 'byteReads', 'selftestMs', 'warmupMs', 'glError', 'framebufferStatus', 'quadStripDraws'].forEach(function (key) {
var value = includeSynthetic ? probeValue(info, key) : undefined;
if (value === undefined) value = previous[key];
var valid = typeof value === 'number' && isFinite(value) && value >= 0;
if (key !== 'selftestMs' && key !== 'warmupMs') valid = valid && Math.floor(value) === value;
safe[key] = valid ? value : 'not_reported';
});
var profile = includeSynthetic ? probeValue(info, 'profile') : undefined;
var attempts = includeSynthetic ? probeValue(info, 'attemptCount') : undefined;
if (profile === undefined) profile = previous.profile;
if (attempts === undefined) attempts = previous.attemptCount;
safe.profile = ['default', 'packed_byte', 'unpacked_byte'].indexOf(profile) >= 0 ? profile : 'not_reported';
safe.attemptCount = typeof attempts === 'number' && Math.floor(attempts) === attempts && attempts >= 1 && attempts <= 3 ? attempts : 'not_reported';
var nativeStrategy = includeSynthetic ? probeValue(info, 'nativeStrategy') : undefined;
var nativeAttempts = includeSynthetic ? probeValue(info, 'nativeAttempts') : undefined;
if (nativeStrategy === undefined) nativeStrategy = previous.nativeStrategy;
if (nativeAttempts === undefined) nativeAttempts = previous.nativeAttempts;
safe.nativeStrategy = ['standard', 'buffer_copy', 'framebuffer_y', 'buffer_copy_framebuffer_y'].indexOf(nativeStrategy) >= 0 ? nativeStrategy : 'not_reported';
safe.nativeAttempts = typeof nativeAttempts === 'number' && Math.floor(nativeAttempts) === nativeAttempts && nativeAttempts >= 1 && nativeAttempts <= 8 ? nativeAttempts : 'not_reported';
var drawStrategy = includeSynthetic ? probeValue(info, 'drawStrategy') : undefined;
if (drawStrategy === undefined) drawStrategy = previous.drawStrategy;
safe.drawStrategy = ['indexed', 'quad_strip'].indexOf(drawStrategy) >= 0 ? drawStrategy : 'not_reported';
[['nativeInitialStage', NATIVE_GPU_STAGES], ['nativeInitialCode', NATIVE_GPU_CODES]].forEach(function (entry) {
var value = includeSynthetic ? probeValue(info, entry[0]) : undefined;
if (value === undefined) value = previous[entry[0]];
safe[entry[0]] = entry[1].indexOf(value) >= 0 ? value : 'not_reported';
});
safe.matmul = probeSyntheticMetadata(includeSynthetic ? probeValue(info, 'matmul') : undefined, previous.matmul);
safe.conv = probeSyntheticMetadata(includeSynthetic ? probeValue(info, 'conv') : undefined, previous.conv);
safe.nativeProbe = probeNativeGpuMetadata(includeSynthetic ? probeValue(info, 'nativeProbe') : undefined, previous.nativeProbe);
return safe;
}
function isGPUFailure(error) {
var code = probeValue(error, 'code');
return typeof code === 'string' && (code.indexOf('NATIVE_FACE_WEBGL_') === 0 || code === 'NATIVE_FACE_BACKEND_CONFLICT');
}
function probeRawMetadata(frame) {
var data = probeValue(frame, 'data'), length = 0;
try {
if (ArrayBuffer.isView(data)) length = data.byteLength;
else if (data) length = Object.getOwnPropertyDescriptor(ArrayBuffer.prototype, 'byteLength').get.call(data);
} catch (_) {}
var format = probeValue(frame, 'pixelFormat') || probeValue(frame, 'format'), rotation = probeValue(frame, 'rotation');
return { width: probeNumber(probeValue(frame, 'width')), height: probeNumber(probeValue(frame, 'height')), byteLength: probeNumber(length),
format: typeof format === 'string' && /^(RGBA|BGRA|ARGB|ABGR)(8888)?$/i.test(format) ? format.toUpperCase() : format === undefined ? 'not_reported' : 'unrecognized',
rotation: typeof rotation === 'number' && isFinite(rotation) && Math.abs(rotation) <= 360 ? rotation : 'not_reported' };
}
function probeCode(error) {
var code = probeValue(error, 'code');
if (code === 'none') return 'none';
if (typeof code === 'string' && code.indexOf('NATIVE_FACE_WEBGL_') === 0 && GPU_CODES.indexOf(code.slice(18)) >= 0) return code;
return ['NATIVE_FACE_BACKEND', 'NATIVE_FACE_BACKEND_CONFLICT', 'NATIVE_FACE_INIT_TIMEOUT', 'NATIVE_FACE_FRAME', 'NATIVE_FACE_FORMAT', 'NATIVE_FACE_STRIDE', 'NATIVE_FACE_ROTATION', 'NATIVE_FACE_DISPOSED', 'NATIVE_FACE_BUSY'].indexOf(code) >= 0 ? code : 'unavailable';
}
function probeCapability(name) {
try { return typeof my.canIUse !== 'function' ? 'unavailable' : my.canIUse(name) ? 'yes' : 'no'; } catch (_) { return 'error'; }
}
// Keep the logical lens used by the guide separate from the AR compatibility
// request. This mapping is a host setting, not a claim about actual hardware.
function arDevicePosition(logicalPosition) {
var front = config.arFrontPosition === 'back' ? 'back' : 'front';
return logicalPosition === 'back' ? (front === 'front' ? 'back' : 'front') : front;
}
var STEPS = [
{ target: 'front', tip: '请正脸出镜', turn: '正脸居中', mask: 0, done: '正脸拍摄完成' },
{ target: 'right', tip: '请右脸出镜', turn: '右脸居中', mask: 2, done: '右脸拍摄完成', next: '下一张:右脸出镜\n请向左转头' },
{ target: 'left', tip: '请左脸出镜', turn: '左脸居中', mask: 1, done: '左脸拍摄完成', next: '下一张:左脸出镜\n请向右转头' }
];
Page(require('../../utils/page-header').page({
onShareAppMessage: function () { return require('../../utils/share').message(); },
data: { stepIndex: 0, stepText: '1/3', stepTip: STEPS[0].tip, guides: ['正脸居中', '取下眼镜', '光线充足'],
maskStep: 0, maskName: 'front', maskInsetX: 0, maskInsetY: 0,
maskStageStyle: 'width:375px;height:726px;', scanStageStyle: 'width:375px;height:726px;',
devicePosition: 'front', arDevicePosition: 'front', cameraSession: 0, camError: false, previewUrl: '', doneTip: '', nextTip: '', analyzing: false,
countdown: 0, fillLight: false, analysisStage: '', analysisError: '', scanError: false,
useAR: config.captureMode === 'ar', useNativeFace: config.captureMode === 'native-face',
autoCapture: config.captureMode === 'ar' || config.captureMode === 'native-face', nativeCameraId: 'skin-camera', nativeFrameSize: nativeFrameSize(config.nativeFrameSize), liveHint: '请将脸部移入取景框', liveGood: false,
identityPending: false, identityError: '',
compressWidth: 1, compressHeight: 1, cameraEnabled: false, permissionMessage: '',
debugMode: false, debugComplete: false, debugPhotos: [], uploadOnly: false,
localFaceProbe: false, captureProbeVersion: CAPTURE_PROBE_VERSION, probeToolbarTop: 112,
showDiagnose: config.environment !== 'production' && config.cameraDebug === true, diagText: '', cameraSupported: false },
onLoad: function (query) {
this.alive = true; this.visible = true; this.photos = []; this.busy = false; this.seq = 0;
this.layout();
var supported = config.nativeCamera === true && (this.data.useAR
? typeof my.createArCameraContext === 'function'
: (typeof permissions.cameraSupported === 'function' ? permissions.cameraSupported() : true));
this.cameraSupported = supported;
this.debugKind = query && query.debug === 'local-face' && typeof development.nativeFaceProbeEnabled === 'function' && development.nativeFaceProbeEnabled() ? 'local-face'
: query && query.debug === 'camera' && development.cameraDebugEnabled() ? 'camera' : '';
this.debugMode = !!this.debugKind; this.localFaceProbe = this.debugKind === 'local-face';
var uploadOnly = config.photoInputMode === 'upload' && !this.debugMode;
if (uploadOnly) { supported = false; this.cameraSupported = false;
this.setData({ uploadOnly: true, autoCapture: false, useAR: false, useNativeFace: false }); }
this.probeFramesProcessed = 0; this.probeAutoCaptured = 0; this.probeStage = 'initializing'; this.probeFailureStage = 'none'; this.probeFailureCode = 'none';
this.nativeFrameCallbacks = 0; this.nativeFramesSkippedInFlight = 0; this.nativeAcceptedCalls = 0;
if (this.localFaceProbe) this.probeRuntime = arCamera.diagnostics.system(my);
this.setData({ debugMode: this.debugMode, localFaceProbe: this.localFaceProbe, probeToolbarTop: arCamera.diagnostics.toolbarTop(my),
cameraSupported: supported, devicePosition: 'front', arDevicePosition: arDevicePosition('front') });
if (this.debugMode) {
if (!supported) {
this.probeStage = 'failed'; this.probeFailureStage = 'listener';
this.setData({ camError: true, liveHint: '实时相机能力检查未通过', permissionMessage: '当前运行时未通过相机能力检查,请复制识别诊断;尚未完成本地拍摄验收。' });
}
return;
}
this.ownerId = api.user() && api.user().id;
var app = typeof getApp === 'function' ? getApp() : null;
if (!api.user() || !app || !app.globalData.profile) { this.loadBlocked = true; my.redirectTo({ url: '/pages/splash/splash' }); return; }
if (!supported && !uploadOnly) {
// 淘宝 <camera> 组件不可用:直接用系统相机/相册,页面不再挂相机组件(否则整页崩溃)
this.setData({ cameraEnabled: false, camError: true,
permissionMessage: this.data.autoCapture ? '当前设备未提供实时识别相机,请打开相机测试检查;尚未完成自动拍摄。' : '请用下方按钮拍摄,三张依次为正脸、右脸、左脸。' });
}
},
onReady: function () {
this.pageReady = true;
if (this.loadBlocked) return;
this.layout();
if (this.cameraAuthorized) this.mountCamera();
},
onShow: function () {
this.visible = true;
if (this.loadBlocked) return;
var that = this;
if (!this.debugMode && typeof api.restore === 'function') {
this.setData({ identityPending: true, identityError: '' });
api.restore().then(function () {
if (!that.alive || !that.visible) return;
that.setData({ identityPending: false }); that.resumeVisible();
}).catch(function (err) {
if (!that.alive || !that.visible) return;
if (!api.user()) { that.onCancel(); return; }
that.setData({ identityError: err.message || '暂时无法确认登录状态,请重试' });
});
return;
}
this.resumeVisible();
},
onRetryIdentity: function () { this.onShow(); },
debugGateEnabled: function () {
return this.debugKind === 'local-face' ? typeof development.nativeFaceProbeEnabled === 'function' && development.nativeFaceProbeEnabled()
: this.debugKind === 'camera' && development.cameraDebugEnabled();
},
onCopyCaptureDiagnostics: function () {
if (!this.alive || !this.localFaceProbe || !this.debugGateEnabled()) return;
this.readNativeEngineDiagnostics(this.nativeEngineRecord);
var last = this.probeDetection || {}, meta = this.probeFrame || {}, verdict = this.latestNativeVerdict;
var engine = this.probeEngine || probeEngineMetadata(), timings = this.probeTimings || probeTimingMetadata(), requested = nativeEngineOptions();
var gpu = this.probeGPU || probeGpuMetadata({ requested: requested.backend === 'webgl' });
var stage = ['initializing', 'listening', 'detecting', 'capturing', 'complete', 'failed'].indexOf(this.probeStage) >= 0 ? this.probeStage : 'initializing';
var failureStage = ['init', 'listener', 'frame', 'photo', 'performance', 'none'].indexOf(this.probeFailureStage) >= 0 ? this.probeFailureStage : 'none';
var reason = ['ok', 'frame', 'angle', 'quality', 'unavailable'].indexOf(last.reason) >= 0 ? last.reason : 'unavailable';
var format = typeof meta.format === 'string' && /^(RGBA|BGRA|ARGB|ABGR)(8888)?$/.test(meta.format) ? meta.format : meta.format === 'unrecognized' ? 'unrecognized' : 'not_reported';
var rotation = typeof meta.rotation === 'number' && isFinite(meta.rotation) && Math.abs(meta.rotation) <= 360 ? meta.rotation : 'not_reported';
var lines = [CAPTURE_PROBE_VERSION].concat(this.probeRuntime || []).concat(['mode=local-face; captureMode=native-face',
'frameSize.requested=' + nativeFrameSize(this.data.nativeFrameSize),
'engine.requested.backend=' + requested.backend,
'engine.requested.inputSize=' + requested.inputSize + '; maxSide=' + requested.maxSide + '; landmarkModel=' + requested.landmarkModel,
'engine.actual.backend=' + engine.backend + '; inputSize=' + engine.inputSize + '; maxSide=' + engine.maxSide + '; landmarkModel=' + engine.landmarkModel + '; cpuOptimization=' + engine.cpuOptimization,
'stage=' + stage + '; failureStage=' + failureStage,
'typeof.createCameraContext=' + typeof my.createCameraContext,
'typeof.createOffscreenCanvas=' + typeof my.createOffscreenCanvas + '; typeof.createCanvas=' + typeof my.createCanvas + '; typeof.createWorker=' + typeof my.createWorker,
'canIUse.camera=' + probeCapability('camera') + '; canIUse.createCameraContext=' + probeCapability('createCameraContext'),
'logicalPosition=' + (this.data.devicePosition === 'back' ? 'back' : 'front') + '; step=' + (this.data.stepIndex + 1),
'framesProcessed=' + probeNumber(this.probeFramesProcessed) + '; inflight=' + !!this.nativeInference,
'frameEvents.total=' + probeNumber(this.nativeFrameCallbacks) + '; skippedInFlight=' + probeNumber(this.nativeFramesSkippedInFlight) + '; acceptedCalls=' + probeNumber(this.nativeAcceptedCalls),
'currentDetection.elapsedMs=' + probeNumber(last.elapsedMs) + '; faceCount=' + probeNumber(last.faceCount),
'currentDetection.sourceAgeMs=' + probeNumber(last.sourceAgeMs),
'quality.reason=' + reason + '; quality.code=' + probeCode({ code: last.code || this.probeFailureCode || 'none' }) + '; validFresh=' + !!(verdict && verdict.ok && Date.now() - this.latestNativeAt <= 1500),
'slowResults=' + probeNumber(this.nativeSlowResults) + '; waiting=' + !!this.nativeCaptureWait,
'autoCapturedPhotoCount=' + probeNumber(this.probeAutoCaptured) + '; done=' + !!this.data.debugComplete,
'frame.width=' + probeNumber(meta.width) + '; frame.height=' + probeNumber(meta.height) + '; frame.byteLength=' + probeNumber(meta.byteLength) + '; frame.format=' + format + '; frame.rotation=' + rotation]);
lines.push('gpu.requested=' + gpu.requested + '; selftest=' + gpu.selftest + '; stage=' + gpu.stage + '; code=' + gpu.code + '; contextLost=' + gpu.contextLost);
lines.push('gpu.matmulDraws=' + gpu.matmulDraws + '; convDraws=' + gpu.convDraws + '; pixelReads=' + gpu.pixelReads + '; selftestMs=' + gpu.selftestMs + '; warmupMs=' + gpu.warmupMs);
lines.push('gpu.profile=' + gpu.profile + '; attemptCount=' + gpu.attemptCount);
lines.push('gpu.nativeStrategy=' + gpu.nativeStrategy + '; nativeAttempts=' + gpu.nativeAttempts);
lines.push('gpu.drawStrategy=' + gpu.drawStrategy + '; quadStripDraws=' + gpu.quadStripDraws);
lines.push('gpu.nativeInitialStage=' + gpu.nativeInitialStage + '; nativeInitialCode=' + gpu.nativeInitialCode);
lines.push('gpu.synthetic.floatReads=' + gpu.floatReads + '; byteReads=' + gpu.byteReads);
lines.push('gpu.synthetic.glError=' + gpu.glError + '; framebufferStatus=' + gpu.framebufferStatus);
var nativeProbe = gpu.nativeProbe;
lines.push('gpu.native.stage=' + nativeProbe.stage + '; code=' + nativeProbe.code + '; clear=' + nativeProbe.clear + '; shader=' + nativeProbe.shader + '; byteTexture=' + nativeProbe.byteTexture + '; floatTexture=' + nativeProbe.floatTexture);
lines.push('gpu.native.constantArrays=' + nativeProbe.constantArrays + '; constantIndexed=' + nativeProbe.constantIndexed + '; constantQuad=' + nativeProbe.constantQuad);
lines.push('gpu.native.vertexBufferBytes=' + nativeProbe.vertexBufferBytes + '; indexBufferBytes=' + nativeProbe.indexBufferBytes);
lines.push('gpu.native.pixel0=' + nativeProbe.pixel0 + '; pixel1=' + nativeProbe.pixel1 + '; pixel2=' + nativeProbe.pixel2 + '; pixel3=' + nativeProbe.pixel3);
lines.push('gpu.native.changedBytes=' + nativeProbe.changedBytes + '; mismatchCount=' + nativeProbe.mismatchCount + '; expectedChecksum=' + nativeProbe.expectedChecksum + '; actualChecksum=' + nativeProbe.actualChecksum);
lines.push('gpu.native.glError=' + nativeProbe.glError + '; framebufferStatus=' + nativeProbe.framebufferStatus + '; readReturn=' + nativeProbe.readReturn);
['matmul', 'conv'].forEach(function (operation) {
GPU_SYNTHETIC_FIELDS.forEach(function (key) { lines.push('gpu.synthetic.' + operation + '.' + key + '=' + gpu[operation][key]); });
});
['snapshotMs', 'modelMs', 'tensorMs', 'detectorMs', 'landmarkMs', 'totalMs'].forEach(function (key) { lines.push('timings.' + key + '=' + timings[key]); });
var report = lines.join('\n'); this.setData({ diagText: report });
try { if (typeof my.setClipboard === 'function') my.setClipboard({ text: report, success: function () {}, fail: function () {} }); } catch (_) {}
},
resumeVisible: function () {
if (!this.debugMode && this.ownerId !== (api.user() && api.user().id)) { this.onCancel(); return; }
if (this.pendingChoice) { this.acceptChosenPhoto(); return; }
if (this.choosing) return;
if (this.data.analyzing) { this.submitPhotos(); return; }
if (this.data.doneTip) { this.busy = false; clearTimeout(this.nextTimer); }
if (!this.data.previewUrl && !this.data.analyzing && !this.data.debugComplete) this.renderStep();
if (!this.cameraSupported) return; // 平台没有内置相机:直接用系统相机/相册
var that = this;
permissions.settings().then(function (current) {
if (!that.alive || !that.visible) return;
if (permissions.state(current, 'scope.camera') === true) { that.cameraAuthorized = true; that.mountCamera(); }
else if (permissions.state(current, 'scope.camera') === false) that.setData({ cameraEnabled: false, permissionMessage: '相机权限未开启,可前往授权设置或使用相册。' });
else that.onEnableCamera(); // First visit follows the main flow into the platform permission prompt.
}).catch(function () {});
},
layout: function () {
var info = {};
try { if (typeof my.getSystemInfoSync === 'function') info = my.getSystemInfoSync(); } catch (_) {}
var w = info.windowWidth || 375, h = Math.max(1, (info.windowHeight || 726) - (this.data.brandHeader ? this.data.brandHeader.height : 0));
this.applyCameraLayout(w, h);
// The native title bar can change the drawable viewport after onLoad.
// Use the camera's mounted bounds for both its mask and framing checks.
if (!this.pageReady || typeof my.createSelectorQuery !== 'function') return;
var that = this;
try {
my.createSelectorQuery().select('.cam').boundingClientRect().exec(function (rows) {
var rect = rows && rows[0];
if (!that.alive || !that.visible || !rect || !(rect.width > 0) || !(rect.height > 0)) return;
that.applyCameraLayout(rect.width, rect.height);
});
} catch (_) {}
},
applyCameraLayout: function (w, h) {
this.viewWidth = w; this.viewHeight = h;
var scale = Math.min(w / 375, h / 726), width = 375 * scale, height = 726 * scale;
var scan = Math.min(w / 1080, h / 2091);
this.setData({ maskInsetX: (w - width) / 2, maskInsetY: (h - height) / 2,
maskStageStyle: 'width:' + width + 'px;height:' + height + 'px;',
scanStageStyle: 'width:' + (1080 * scan) + 'px;height:' + (2091 * scan) + 'px;' });
},
mountCamera: function () {
if (!this.alive || !this.visible || !this.pageReady || this.data.cameraEnabled || this.data.debugComplete || this.data.analyzing || this.data.analysisError) return;
if (!this.cameraSupported) return;
var that = this;
var cameraSession = this.data.cameraSession + 1;
var nativeId = this.data.useNativeFace ? 'skin-camera-' + cameraSession : 'skin-camera';
this.setData({ cameraEnabled: true, cameraSession: cameraSession, nativeCameraId: nativeId, camError: false }, function () {
if (!that.alive || !that.visible || !that.data.cameraEnabled || cameraSession !== that.data.cameraSession) return;
try {
if (that.data.useAR) { that.arContext = my.createArCameraContext('skin-ar-camera-' + cameraSession); return; }
that.cameraCtx = my.createCameraContext(nativeId);
if (!that.cameraCtx || !that.cameraCtx.takePhoto) that.onCameraError();
else if (that.data.useNativeFace) that.startNativeFrames(cameraSession);
} catch (_) { that.onCameraError(); }
});
},
isCurrentCameraEvent: function (event) {
var target = event && event.currentTarget, session = target && target.dataset && target.dataset.cameraSession;
var id = target && target.id, match = typeof id === 'string' && /^skin-(?:ar-)?camera-(\d+)$/.exec(id);
if (session === undefined && match) session = match[1];
return session === undefined || Number(session) === this.data.cameraSession;
},
onARInit: function (event) {
if (!this.alive || !this.visible || !this.data.cameraEnabled || !this.isCurrentCameraEvent(event)) return;
this.setData({ liveHint: '请将脸部移入取景框', permissionMessage: '' });
// Skin analysis must use unretouched input. This is a documented AR API.
try { if (this.arContext && typeof this.arContext.faceBeauty === 'function') this.arContext.faceBeauty({ level: 0 }, function () {}); } catch (_) {}
},
onARFrame: function (event) {
if (!this.alive || !this.visible || !this.data.cameraEnabled || !this.isCurrentCameraEvent(event)) return;
var frame = event && (event.detail || event);
this.latestFrame = frame; this.latestFrameAt = Date.now();
if (this.canvasOperation) {
if (!this.data.analyzing && !this.data.analysisError && (this.data.liveHint !== '正在准备相机…' || this.data.liveGood)) this.setData({ liveHint: '正在准备相机…', liveGood: false });
this.holdState = null; return;
}
if (this.busy || this.data.analyzing || this.data.analysisError || this.data.doneTip) return;
if (this.lastDetectAt && Date.now() - this.lastDetectAt < 120) return;
this.lastDetectAt = Date.now();
var verdict = arCamera.evaluate(frame, STEPS[this.data.stepIndex].target, false, { viewWidth: this.viewWidth, viewHeight: this.viewHeight });
this.updateLiveHint(verdict.ok ? (verdict.warning ? verdict.warning + ',保持不动即可拍摄' : '保持不动') : verdict.hint, verdict.ok);
if (!verdict.ok) { this.holdState = null; return; }
var held = arCamera.hold(this.holdState, verdict.observation, Date.now());
this.holdState = held.state;
if (held.ready) this.startAutoCapture();
},
stopNativeFrames: function () {
this.nativeListenerGeneration = (this.nativeListenerGeneration || 0) + 1;
this.nativeListening = false; clearTimeout(this.nativeFrameTimer); clearTimeout(this.nativeInitTimer);
this.clearNativeCaptureWait();
var listener = this.nativeFrameListener; this.nativeFrameListener = null;
this.latestNativeVerdict = null; this.latestNativeAt = 0; this.holdState = null;
try { if (listener && typeof listener.stop === 'function') listener.stop({ success: function () {}, fail: function () {} }); } catch (_) {}
},
nativeCurrent: function (seq, session, generation) {
return this.alive && this.visible && this.data.cameraEnabled && this.seq === seq && this.data.cameraSession === session
&& this.nativeListenerGeneration === generation && this.nativeListening;
},
readNativeEngineDiagnostics: function (record, info) {
if (!this.localFaceProbe) return;
var stats;
try { if (record && record.detector && typeof record.detector.stats === 'function') stats = record.detector.stats(); } catch (_) {}
if (stats) {
this.probeEngine = probeEngineMetadata(stats, this.probeEngine);
this.probeGPU = probeGpuMetadata(probeValue(stats, 'gpu'), this.probeGPU, !!record && !record.ready);
}
if (info) {
this.probeEngine = probeEngineMetadata(info, this.probeEngine);
this.probeGPU = probeGpuMetadata(probeValue(info, 'gpu'), this.probeGPU, !!record && !record.ready);
}
},
retireNativeDetector: function (record) {
record = record || this.nativeEngineRecord;
if (!record || record.retired) return;
record.retired = true;
// dispose is documented by our engine as idempotent and pending-safe. Its
// resources stay alive until its own init/detect operation has settled.
try { if (record.detector && typeof record.detector.dispose === 'function') record.detector.dispose(); } catch (_) {}
if (this.nativeEngineRecord === record) {
this.nativeEngineRecord = null; this.nativeDetector = null; this.nativeInitPromise = null;
}
var that = this, waits = [this.nativeRetirement, record.init, record.inference].filter(function (value) { return !!value; });
var retirement = Promise.all(waits.map(function (value) { return Promise.resolve(value).catch(function () {}); }));
this.nativeRetirement = retirement;
retirement.then(function () { if (that.nativeRetirement === retirement) that.nativeRetirement = null; });
},
ensureNativeDetector: function () {
if (this.nativeInitPromise) {
if (nativeEngineOptions().backend === 'webgl' && this.nativeInferenceCompletion) {
var current = this;
return this.nativeInferenceCompletion.catch(function () {}).then(function () { return current.ensureNativeDetector(); });
}
return this.nativeInitPromise;
}
var that = this, record = { retired: false, detector: null }, barrier = this.nativeRetirement;
this.nativeEngineRecord = record;
var initialization = (barrier ? Promise.resolve(barrier).catch(function () {}) : Promise.resolve()).then(function () {
if (!that.alive || record.retired || that.nativeEngineRecord !== record) throw { canceled: true };
var implementation = require('../../utils/native-face');
if (!implementation || typeof implementation.create !== 'function') throw new Error('本地人脸识别引擎不可用,请重新开启或更新预览');
var options = nativeEngineOptions();
if (that.localFaceProbe) that.probeGPU = probeGpuMetadata({ requested: options.backend === 'webgl', selftest: 'not_run', stage: 'uninitialized', code: 'NONE', contextLost: false });
record.detector = implementation.create(options); that.nativeDetector = record.detector;
if (!record.detector || typeof record.detector.init !== 'function' || typeof record.detector.detect !== 'function') throw new Error('本地人脸识别引擎不完整');
return record.detector.init();
}).then(function (info) {
if (!that.alive || record.retired || that.nativeEngineRecord !== record) throw { canceled: true };
that.readNativeEngineDiagnostics(record, info);
if (nativeEngineOptions().backend === 'webgl') {
var gpu = probeGpuMetadata(probeValue(info, 'gpu'), null, true);
if (probeValue(info, 'backend') !== 'webgl' || gpu.selftest !== 'passed' || gpu.stage !== 'ready' || gpu.contextLost
|| !(gpu.matmulDraws > 0 && gpu.convDraws > 0 && gpu.pixelReads > 0)) {
var error = new Error('WebGL 数值自测或模型热身未通过,已停止实时识别');
error.code = 'NATIVE_FACE_WEBGL_BACKEND_FAILED'; error.gpu = probeValue(info, 'gpu'); throw error;
}
}
record.ready = true;
return record.detector;
}).catch(function (error) {
if (that.nativeEngineRecord === record || !that.nativeEngineRecord) that.readNativeEngineDiagnostics(record, error);
that.retireNativeDetector(record); throw error;
});
record.init = initialization; this.nativeInitPromise = initialization;
return initialization;
},
failNativeCapture: function (message, failureStage) {
if (this.localFaceProbe) { this.probeStage = 'failed'; this.probeFailureStage = ['init', 'listener', 'frame', 'photo', 'performance'].indexOf(failureStage) >= 0 ? failureStage : 'frame'; }
this.stopNativeFrames(); this.seq++; this.busy = false; clearTimeout(this.captureTimer);
this.cameraCtx = null;
this.setData({ cameraEnabled: false, camError: true, countdown: 0, fillLight: false, liveGood: false,
liveHint: message, permissionMessage: message + ';请重新开启实时相机,尚未完成自动拍摄。' });
},
startNativeFrames: function (session) {
this.stopNativeFrames(); this.nativeListening = true; this.lastNativeDetectAt = 0; this.nativeSlowResults = 0;
var that = this, seq = this.seq, generation = this.nativeListenerGeneration, context = this.cameraCtx;
var listenerSetup = false;
if (this.localFaceProbe) {
this.probeStage = 'initializing'; this.probeFailureStage = 'none'; this.probeFailureCode = 'none'; this.probeDetection = null; this.probeTimings = null;
}
this.setData({ liveGood: false, liveHint: nativeEngineOptions().backend === 'webgl' ? '正在校验图形加速并预热模型…' : '正在加载本地人脸识别…', permissionMessage: '' });
if (!context || typeof context.onCameraFrame !== 'function') { this.failNativeCapture('当前相机缺少实时帧监听能力', 'listener'); return; }
this.nativeInitTimer = setTimeout(function () {
if (!that.nativeCurrent(seq, session, generation) || listenerSetup) return;
that.readNativeEngineDiagnostics(that.nativeEngineRecord);
if (that.localFaceProbe) that.probeFailureCode = 'NATIVE_FACE_INIT_TIMEOUT';
that.retireNativeDetector();
that.failNativeCapture('本地识别初始化超过30秒,请复制识别诊断后重试', 'init');
}, 30000);
this.ensureNativeDetector().then(function () {
if (!that.nativeCurrent(seq, session, generation)) return;
clearTimeout(that.nativeInitTimer);
listenerSetup = true; if (that.localFaceProbe) that.probeStage = 'listening';
var received = false;
that.nativeFrameListener = context.onCameraFrame(function (frame) {
if (!that.nativeCurrent(seq, session, generation)) return;
if (!received) { received = true; clearTimeout(that.nativeFrameTimer); }
that.detectNativeFrame(frame, seq, session, generation);
});
var listener = that.nativeFrameListener;
if (!listener || typeof listener.start !== 'function' || typeof listener.stop !== 'function') throw new Error('当前相机未返回可控制的实时帧监听器');
that.nativeFrameTimer = setTimeout(function () {
if (that.nativeCurrent(seq, session, generation) && !received) that.failNativeCapture('相机10秒未返回真实帧', 'listener');
}, 10000);
listener.start({ success: function () {
if (that.nativeCurrent(seq, session, generation) && !received) that.setData({ liveHint: '等待真实相机帧…' });
}, fail: function () { if (that.nativeCurrent(seq, session, generation)) that.failNativeCapture('实时帧监听启动失败', 'listener'); } });
}).catch(function (error) {
if (that.nativeCurrent(seq, session, generation)) {
clearTimeout(that.nativeInitTimer);
if (that.localFaceProbe) that.probeFailureCode = probeCode(error);
that.failNativeCapture(error.message || '本地人脸识别启动失败', listenerSetup ? 'listener' : 'init');
}
});
},
detectNativeFrame: function (frame, seq, session, generation) {
if (!this.nativeCurrent(seq, session, generation)) return;
this.nativeFrameCallbacks++;
if (this.nativeInference) { this.nativeFramesSkippedInFlight++; return; }
if (this.data.analyzing || this.data.analysisError || this.data.doneTip || this.data.identityPending || this.data.previewUrl) return;
if (this.nativePhotoOperation || this.canvasOperation) {
this.holdState = null;
if (this.data.liveGood || this.data.liveHint !== '正在等待上次拍照处理结束…') this.setData({ liveGood: false, liveHint: '正在等待上次拍照处理结束…' });
return;
}
// After the countdown starts, freeze recognition gates until the shutter.
// Normal hand/head motion must never restart an already armed photo.
if (this.busy && !this.countdownVoiceTicket) return;
var frameAt = Date.now();
if (this.lastNativeDetectAt && frameAt - this.lastNativeDetectAt < 120) return;
this.lastNativeDetectAt = frameAt;
this.nativeAcceptedCalls++;
if (this.localFaceProbe) { this.probeFrame = probeRawMetadata(frame); this.probeStage = 'detecting'; }
var that = this, step = this.data.stepIndex, engineRecord = this.nativeEngineRecord;
var operation;
// The in-flight/throttle checks above precede the engine's raw-buffer copy.
try { operation = Promise.resolve(this.nativeDetector.detect(frame)); } catch (error) { operation = Promise.reject(error); }
if (engineRecord) engineRecord.inference = operation;
this.nativeInference = operation;
var completion = operation.then(function (result) {
if (!that.nativeCurrent(seq, session, generation) || that.data.stepIndex !== step || that.data.analyzing || that.data.doneTip || that.nativePhotoOperation) return;
if (that.data.countdown || that.data.fillLight) return;
// Phone verification: front-camera yaw is opposite to the prior raw-frame
// assumption. Reverse its sign for both side steps, preserving guide copy,
// photo order and the existing rear-camera mapping.
var verdict = nativeQuality.evaluate(result, STEPS[step].target, { viewWidth: that.viewWidth, viewHeight: that.viewHeight,
mirrored: that.data.devicePosition === 'front' });
if (that.localFaceProbe) {
that.probeStage = 'listening';
that.probeFramesProcessed++;
that.readNativeEngineDiagnostics(engineRecord, result); that.probeTimings = probeTimingMetadata(result);
that.probeDetection = { elapsedMs: probeNumber(result.elapsedMs), faceCount: probeNumber(verdict.faceCount),
sourceAgeMs: probeNumber(Date.now() - frameAt),
reason: ['frame', 'angle', 'quality'].indexOf(verdict.reason) >= 0 ? verdict.reason : verdict.ok ? 'ok' : 'unavailable', code: 'none' };
}
that.latestNativeVerdict = verdict; that.latestNativeAt = frameAt;
var fresh = Date.now() - frameAt <= 1500;
that.nativeSlowResults = fresh ? 0 : (that.nativeSlowResults || 0) + 1;
if (that.nativeSlowResults >= 3) { that.failNativeCapture('当前识别链路连续耗时超过1.5秒,已暂停抓拍', 'performance'); return; }
that.updateLiveHint(!fresh ? '本地识别较慢,请保持不动…' : verdict.ok ? (verdict.warning ? verdict.warning + ',保持不动即可拍摄' : '保持不动') : verdict.hint, verdict.ok && fresh);
if (!verdict.ok || !fresh) {
that.holdState = null;
if (that.data.countdown || that.data.fillLight || that.nativeCaptureWait || that.countdownVoiceTicket) {
that.countdownVoiceTicket = null; that.spokenHint = '';
clearTimeout(that.captureTimer); that.clearNativeCaptureWait(); that.busy = false; that.setData({ countdown: 0, fillLight: false }); if (!that.debugMode) voice.stop(true);
}
return;
}
if (that.busy) return;
var held = nativeQuality.hold(that.holdState, verdict.observation, Date.now());
that.holdState = held.state;
if (held.ready) that.startAutoCapture();
}).catch(function (error) {
if (isGPUFailure(error)) {
if (that.nativeEngineRecord === engineRecord || !that.nativeEngineRecord) that.readNativeEngineDiagnostics(engineRecord, error);
that.retireNativeDetector(engineRecord);
}
if (that.localFaceProbe && that.nativeCurrent(seq, session, generation)) that.probeDetection = { elapsedMs: 0, faceCount: 0, reason: 'frame', code: probeCode(error) };
if (that.nativeCurrent(seq, session, generation) && !error.canceled && error.code !== 'NATIVE_FACE_BUSY') that.failNativeCapture(error.message || '本地人脸识别失败');
}).then(function () {
if (engineRecord && engineRecord.inference === completion) engineRecord.inference = null;
if (that.nativeInference === operation) that.nativeInference = null;
if (that.nativeInferenceCompletion === completion) that.nativeInferenceCompletion = null;
});
this.nativeInferenceCompletion = completion;
if (engineRecord) engineRecord.inference = completion;
},
clearNativeCaptureWait: function () {
if (this.nativeCaptureWait) clearTimeout(this.nativeCaptureWait.timer);
this.nativeCaptureWait = null;
},
finishNativeCountdown: function (seq) {
if (!this.alive || !this.visible || seq !== this.seq || !this.data.cameraEnabled || this.data.identityPending) return;
// Pose/freshness was checked before arming. Do not revalidate at zero.
this.captureNativePhoto(seq);
},
captureNativePhoto: function (seq) {
var that = this, context = this.cameraCtx;
if (this.nativePhotoOperation || !context || typeof context.takePhoto !== 'function') { this.failNativeCapture('普通相机无法开始拍照', 'photo'); return; }
if (this.localFaceProbe) this.probeStage = 'capturing';
var operation = { seq: seq, session: this.data.cameraSession, done: false }; this.nativePhotoOperation = operation;
operation.timer = setTimeout(function () {
if (!operation.done && that.alive && that.visible && seq === that.seq) that.setData({ countdown: 0, fillLight: false, liveGood: false,
liveHint: '原生拍照未返回,请退出拍照页后重新进入', permissionMessage: '正在等待原生拍照结束,未开始新的拍照。' });
}, 10000);
function finish(kind, result) {
if (operation.done || that.nativePhotoOperation !== operation) return;
operation.done = true; clearTimeout(operation.timer); that.nativePhotoOperation = null;
if (!that.alive || !that.visible || seq !== that.seq || operation.session !== that.data.cameraSession) return;
var file = result && (result.tempImagePath || result.tempFilePath || result.apFilePath);
if (kind !== 'success' || typeof file !== 'string' || !file) { that.failNativeCapture('原生相机未成功返回照片', 'photo'); return; }
if (that.localFaceProbe) that.probeAutoCaptured++;
if (that.localFaceProbe) that.probeStage = 'listening';
that.busy = false; that.confirmPhoto(file);
}
try { context.takePhoto({ quality: 'high', success: function (result) { finish('success', result); }, fail: function (error) { finish('fail', error); }, complete: function () { finish('complete'); } }); }
catch (_) { finish('fail'); }
},
startAutoCapture: function () {
if (this.busy || !this.data.autoCapture || !this.data.cameraEnabled) return;
if (this.data.useNativeFace && (this.nativePhotoOperation || !this.latestNativeVerdict || !this.latestNativeVerdict.ok || Date.now() - this.latestNativeAt > 1500)) return;
if (this.canvasOperation) { this.setData({ liveHint: '正在准备相机…', liveGood: false }); return; }
var that = this, seq = this.seq, left = 3;
this.busy = true; this.holdState = null;
if (!this.debugMode) {
if (typeof voice.dropHint === 'function') voice.dropHint();
}
function tick() {
if (!that.alive || !that.visible || seq !== that.seq) return;
that.setData({ countdown: left });
if (left-- > 0) { that.captureTimer = setTimeout(tick, 1000); return; }
that.setData({ fillLight: true });
that.captureTimer = setTimeout(function () {
if (!that.alive || !that.visible || seq !== that.seq) return;
if (that.data.useNativeFace) {
that.finishNativeCountdown(seq); return;
}
if (!that.latestFrame || Date.now() - that.latestFrameAt > 1500) {
that.busy = false; that.setData({ fillLight: false, liveHint: '实时相机已暂停,请重新开启' }); return;
}
var latest = arCamera.evaluate(that.latestFrame, STEPS[that.data.stepIndex].target, false,
{ viewWidth: that.viewWidth, viewHeight: that.viewHeight });
if (!latest.ok) {
that.busy = false; that.holdState = null;
that.setData({ countdown: 0, fillLight: false, liveGood: false, liveHint: latest.hint }); return;
}
var frame = that.latestFrame;
that.runCanvas(function () {
if (!that.alive || !that.visible || seq !== that.seq) throw { canceled: true };
return image.captureFrameToBase64(frame, that, { defaultPixelFormat: config.arPixelFormat });
}).then(function (photo) {
if (!that.alive || !that.visible || seq !== that.seq) return;
that.preparedPhotos = that.preparedPhotos || []; that.preparedPhotos[that.data.stepIndex] = photo;
that.busy = false; that.confirmPhoto(photo);
}).catch(function (error) {
if (!that.alive || seq !== that.seq) return;
that.busy = false; that.setData({ countdown: 0, fillLight: false, liveHint: error.message, liveGood: false });
// Stop automatic retries after an unsupported pixel layout; a new AR
// session or explicit retry is required, avoiding endless countdowns.
that.setData({ cameraEnabled: false, permissionMessage: error.message });
});
}, 300);
}
if (this.debugMode || typeof voice.afterCurrent !== 'function') {
if (!this.debugMode) voice.play(voice.VOICE.countdown);
tick(); return;
}
// Let the step introduction finish before the spoken/visual countdown.
// A stalled audio host gets only a bounded delay and cannot block photos.
var ticket = {}; this.countdownVoiceTicket = ticket;
function begin() {
if (that.countdownVoiceTicket !== ticket || !that.alive || !that.visible || seq !== that.seq || !that.busy) return;
that.countdownVoiceTicket = null; clearTimeout(that.captureTimer);
if (that.data.useNativeFace && (!that.latestNativeVerdict || !that.latestNativeVerdict.ok || Date.now() - that.latestNativeAt > 1500)) {
that.busy = false; that.holdState = null; return;
}
voice.play(voice.VOICE.countdown, { cut: true }); tick();
}
this.captureTimer = setTimeout(begin, 3000);
voice.afterCurrent(begin);
},
// Native canvas operations cannot be cancelled by hiding/remounting a camera.
// Keep their promise across pause/restart, and serialize both raw AR exports
// and ordinary image compression on this page's single canvas.
runCanvas: function (work) {
var that = this, previous = this.canvasOperation;
var operation = (previous ? previous.catch(function () {}) : Promise.resolve()).then(work);
this.canvasOperation = operation;
function release() {
if (that.canvasOperation !== operation) return;
that.canvasOperation = null;
if (that.alive && that.visible && that.data.cameraEnabled && !that.busy && !that.data.analyzing) {
that.setData({ liveHint: '请将脸部移入取景框', liveGood: false });
}
}
return operation.then(function (value) { release(); return value; }, function (error) { release(); throw error; });
},
onEnableCamera: function () {
if (this.authorizing) return;
var that = this; this.authorizing = true;
this.setData({ camError: false, permissionMessage: '' });
permissions.ensure('scope.camera').then(function () { that.cameraAuthorized = true; that.mountCamera(); }).catch(function (err) {
if (that.alive && that.visible) that.setData({ camError: true, permissionMessage: err.message });
}).then(function () { that.authorizing = false; });
},
onOpenSettings: function () {
var that = this;
permissions.open().then(function (current) {
if (permissions.state(current, 'scope.camera') === true) { that.cameraAuthorized = true; that.mountCamera(); }
}).catch(function (err) { if (that.alive) my.showToast({ content: err.message, type: 'none' }); });
},
onCloseDiagnose: function () { this.setData({ diagText: '' }); },
// 开发版:把真机上的相机能力/授权返回值直接打在页面上,供截图定位
onDiagnose: function () {
if (this.localFaceProbe) { this.onCopyCaptureDiagnostics(); return; }
var that = this;
this.setData({ diagText: '检测中…' });
permissions.diagnose().then(function (info) {
if (that.alive) that.setData({ diagText: info.concat(['logicalPosition=' + that.data.devicePosition,
'requestedPosition=' + (that.data.useAR ? that.data.arDevicePosition : that.data.devicePosition), 'actualPosition=未核验']).join('\n') });
}).catch(function (err) {
if (that.alive) that.setData({ diagText: 'diagnose error: ' + (err && err.message ? err.message : String(err)) });
});
},
onHide: function () { this.pause(); },
onUnload: function () {
this.pause(); this.alive = false; this.photos = []; this.preparedPhotos = []; this.reportPhotos = []; this.debugResults = [];
if (this.nativePhotoOperation) clearTimeout(this.nativePhotoOperation.timer);
this.retireNativeDetector();
this.pendingChoice = null; this.choiceSeq = (this.choiceSeq || 0) + 1;
if (!this.debugMode && typeof api.discardAnalysis === 'function') api.discardAnalysis(this.attempt);
this.attempt = null;
},
pause: function () {
this.visible = false; this.seq++; this.busy = false;
this.countdownVoiceTicket = null;
this.stopNativeFrames();
clearTimeout(this.nextTimer); clearTimeout(this.captureTimer);
if (this.signal) this.signal.canceled = true;
if (!this.debugMode) voice.stop(true);
this.cameraCtx = null;
this.arContext = null; this.latestFrame = null; this.holdState = null;
if (this.alive) this.setData({ cameraEnabled: false, countdown: 0, fillLight: false });
},
renderStep: function () {
var cfg = STEPS[this.data.stepIndex];
this.setData({ stepText: (this.data.stepIndex + 1) + '/3', stepTip: cfg.tip,
guides: [cfg.turn, '取下眼镜', '光线充足'], maskStep: cfg.mask, maskName: cfg.target, doneTip: '', nextTip: '' });
this.spokenHint = '';
if (!this.debugMode) {
if (typeof voice.dropHint === 'function') voice.dropHint();
voice.play(voice.VOICE.step[this.data.stepIndex]);
}
this.holdState = null; this.latestNativeVerdict = null; this.latestNativeAt = 0;
},
updateLiveHint: function (text, good) {
this.setData({ liveHint: text, liveGood: good });
if (this.debugMode || text === this.spokenHint) return;
this.spokenHint = text;
var id = voice.VOICE.hint && voice.VOICE.hint[text];
if (id) voice.play(id, { hint: true });
else if (typeof voice.dropHint === 'function') voice.dropHint();
},
onCameraError: function (event) {
if (!this.alive || !this.visible) return;
if (event && (!this.data.cameraEnabled || !this.isCurrentCameraEvent(event))) return;
if (this.localFaceProbe) { this.probeStage = 'failed'; this.probeFailureStage = 'listener'; }
// Stopping a native camera invalidates a pending shutter/countdown/export.
// An already-submitted analysis has a separate lifetime and must continue.
if (!this.data.analyzing && !this.submitting) { this.seq++; this.busy = false; }
this.stopNativeFrames();
clearTimeout(this.captureTimer);
this.latestFrame = null; this.latestFrameAt = 0; this.lastDetectAt = 0; this.holdState = null;
this.arContext = null; this.cameraCtx = null;
this.setData({ camError: true, cameraEnabled: false, countdown: 0, fillLight: false, liveGood: false,
permissionMessage: this.data.useNativeFace ? '实时相机暂不可用,请检查相机权限后重新开启;尚未完成自动拍摄。' : '相机暂不可用,请检查淘宝的系统相机权限,或使用系统相机/相册。' });
my.showToast({ content: this.data.useNativeFace ? '实时相机不可用,请重新开启' : '相机不可用,请使用系统相机或相册', type: 'none' });
},
onCameraStop: function (event) { if (this.alive && this.visible && this.data.cameraEnabled && this.isCurrentCameraEvent(event)) this.onCameraError(event); },
onShutter: function () {
if (this.busy || this.data.previewUrl || this.data.analyzing || this.data.doneTip || this.data.analysisError) return;
if (this.data.autoCapture) { my.showToast({ content: '实时相机将在姿态合适时自动拍摄,请开启相机并保持面部在框内', type: 'none' }); return; }
if (this.data.camError || !this.cameraCtx) return this.choose(['camera']);
var that = this, seq = this.seq;
this.busy = true;
this.cameraCtx.takePhoto({ quality: 'high', success: function (res) {
if (!that.alive || !that.visible || seq !== that.seq) return;
var file = res.tempImagePath || res.tempFilePath || res.apFilePath;
if (file) {
that.busy = false;
if (that.debugMode) that.setData({ previewUrl: file });
else that.confirmPhoto(file);
}
else my.showToast({ content: '拍摄失败,请重试', type: 'none' });
}, fail: function () {
if (!that.alive || !that.visible || seq !== that.seq) return;
// 真机上内置相机 takePhoto 失败(机型/组件差异、权限被拒等):
// 不打断流程,直接退回系统相机走 my.chooseImage 那条已验证过的路。
that.busy = false;
my.showToast({ content: '内置相机不可用,改用系统相机', type: 'none' });
that.choose(['camera']);
}, complete: function () {
if (seq === that.seq && !that.data.doneTip && !that.data.analyzing && !that.data.previewUrl) that.busy = false;
} });
},
choose: function (sourceType) {
if (this.localFaceProbe) return;
if (this.busy || this.choosing || this.data.previewUrl || this.data.analyzing || this.data.doneTip || this.data.analysisError || this.data.identityPending) return;
var that = this, ticket = this.choiceSeq = (this.choiceSeq || 0) + 1;
this.busy = true; this.choosing = true;
// Camera capture must not depend on unrelated album authorization.
permissions.ensure(sourceType[0] === 'camera' ? 'scope.camera' : 'scope.album').then(function () {
if (!that.alive || !that.visible || ticket !== that.choiceSeq) { that.busy = false; that.choosing = false; return; }
my.chooseImage({ count: 1, sourceType: sourceType,
success: function (res) {
var paths = res.apFilePaths || res.tempFilePaths || [];
if (that.alive && ticket === that.choiceSeq && paths[0] && (that.debugMode || that.ownerId === (api.user() && api.user().id))) {
that.pendingChoice = paths[0];
if (that.visible && !that.data.identityPending) that.acceptChosenPhoto();
}
}, fail: function () { if (that.alive && that.visible) my.showToast({ content: '未选取照片,可重试或检查系统权限', type: 'none' }); },
complete: function () {
if (!that.alive || ticket !== that.choiceSeq) return;
that.choosing = false;
if (!that.data.doneTip && !that.data.analyzing && !that.data.previewUrl) that.busy = false;
if (that.pendingChoice && that.visible && !that.data.identityPending) that.acceptChosenPhoto();
} });
}).catch(function (err) {
that.busy = false; that.choosing = false;
if (that.alive && that.visible) { that.setData({ permissionMessage: err.message }); my.showToast({ content: err.message, type: 'none' }); }
});
},
acceptChosenPhoto: function () {
if (!this.pendingChoice || !this.alive || !this.visible || this.data.identityPending || this.data.analyzing) return;
if (!this.debugMode && this.ownerId !== (api.user() && api.user().id)) { this.pendingChoice = null; return; }
var photo = this.pendingChoice; this.pendingChoice = null; this.busy = false;
if (this.debugMode) this.setData({ previewUrl: photo });
else this.confirmPhoto(photo);
},
onSystemCamera: function () { if (this.localFaceProbe) return; this.choose(['camera']); },
onAlbum: function () { if (this.localFaceProbe) return; this.choose(['album']); },
onFlip: function () {
if (!this.alive || !this.visible || (this.busy && !this.data.countdown && !this.nativeCaptureWait) || this.choosing || this.data.identityPending ||
this.data.previewUrl || this.data.analyzing || this.data.doneTip || this.data.analysisError) return;
if (!this.data.cameraEnabled) { my.showToast({ content: '请在系统相机内切换前后镜头', type: 'none' }); return; }
var that = this, nextPosition = this.data.devicePosition === 'front' ? 'back' : 'front';
// Changing the requested lens starts a fresh native component. Invalidate
// the countdown and old frame/export callbacks before releasing the camera.
this.seq++; this.busy = false;
this.stopNativeFrames();
clearTimeout(this.captureTimer);
this.latestFrame = null; this.latestFrameAt = 0; this.lastDetectAt = 0; this.holdState = null;
this.arContext = null; this.cameraCtx = null; if (!this.debugMode) voice.stop(true);
this.setData({ cameraEnabled: false, devicePosition: nextPosition, arDevicePosition: arDevicePosition(nextPosition), countdown: 0, fillLight: false,
liveGood: false, liveHint: '请将脸部移入取景框', permissionMessage: '' }, function () {
that.mountCamera();
});
},
onRetake: function () { if (!this.busy) this.setData({ previewUrl: '' }); },
onConfirm: function () {
this.confirmPhoto(this.data.previewUrl);
},
confirmPhoto: function (photo) {
if (this.busy || typeof photo !== 'string' || !photo || this.data.analyzing) return;
var that = this, step = this.data.stepIndex;
this.busy = true; this.photos[step] = photo;
if (!this.debugMode) voice.play(voice.VOICE.shot, { cut: true });
if (step === 2) { this.submitPhotos(); return; }
// Automatic acceptance never mounts the manual review overlay. Clear the
// flash/countdown in the same render that shows the next-step transition.
this.setData({ stepIndex: step + 1, previewUrl: '', countdown: 0, fillLight: false,
doneTip: STEPS[step].done, nextTip: STEPS[step + 1].next });
this.nextTimer = setTimeout(function () {
if (!that.alive || !that.visible) return;
that.busy = false; that.renderStep();
}, 2000);
},
submitPhotos: function () {
if (this.submitting || !this.photos[0] || !this.photos[1] || !this.photos[2]) return;
var that = this, seq = ++this.seq, dataUrls = [];
var debugResults = [];
this.signal = { canceled: false }; var signal = this.signal;
this.submitting = true;
this.stopNativeFrames();
// All three photos are already captured. Release the live camera instead
// of decoding/recognizing frames behind the report waiting screen.
this.cameraCtx = null; this.arContext = null; this.latestFrame = null; this.holdState = null;
clearTimeout(this.captureTimer);
this.busy = true; this.setData({ analyzing: true, cameraEnabled: false, previewUrl: '', countdown: 0, fillLight: false,
doneTip: '', analysisError: '', analysisStage: '正在处理照片…' });
if (!this.debugMode) voice.play(voice.VOICE.analyzing);
var existing = this.attempt && this.attempt.profile;
// One shared native canvas: draw the photos sequentially.
(existing ? Promise.resolve() : this.photos.reduce(function (chain, photo, index) {
return chain.then(function () {
if (signal.canceled) throw { canceled: true };
if (that.alive) that.setData({ analysisStage: '正在处理第 ' + (index + 1) + '/3 张照片…' });
return that.preparedPhotos && that.preparedPhotos[index] ? Promise.resolve(that.preparedPhotos[index]) : that.runCanvas(function () {
if (signal.canceled || !that.alive || !that.visible || seq !== that.seq) throw { canceled: true };
return image.compressToBase64(photo, that);
});
}).then(function (value) {
dataUrls.push(value);
if (that.debugMode) debugResults.push({ key: STEPS[debugResults.length].target,
title: STEPS[debugResults.length].tip.replace(/^请/, ''), photo: photo,
width: that.data.compressWidth, height: that.data.compressHeight,
kilobytes: Math.round(value.replace(/^data:[^,]+,/, '').length * 3 / 4 / 1024) });
});
}, Promise.resolve())).then(function () {
if (signal.canceled) throw { canceled: true };
if (that.debugMode) {
if (!that.alive || !that.visible || seq !== that.seq) return;
that.busy = false; that.cameraCtx = null;
dataUrls = []; // Do not keep base64, upload, create an account or touch report state.
if (that.localFaceProbe) that.probeStage = 'complete';
that.setData({ analyzing: false, cameraEnabled: false, debugComplete: true, debugPhotos: debugResults });
return;
}
var ordered = [dataUrls[0], dataUrls[2], dataUrls[1]];
if (!existing) that.reportPhotos = ordered;
that.setData({ analysisStage: existing ? '正在恢复本次检测…' : '正在上传照片并生成报告…' });
if (!that.attempt && typeof api.createAnalysisAttempt === 'function') that.attempt = api.createAnalysisAttempt();
var task = existing ? api.resumeAnalysis(that.attempt, signal) : api.analyze(ordered, getApp().globalData.profile, signal, that.attempt);
return task.then(function (payload) {
if (!that.alive || !that.visible || seq !== that.seq) return;
getApp().globalData.photos = that.reportPhotos; getApp().globalData.payload = payload;
that.setData({ analyzing: false });
voice.stop(true); my.redirectTo({ url: '/pages/report/report' });
});
}).catch(function (err) {
if (err.canceled || !that.alive || !that.visible || seq !== that.seq) return;
that.busy = false; that.setData({ analyzing: false, analysisError: err.message || err.errorMessage || '照片处理失败,请重试' });
if (!that.debugMode) voice.play(voice.VOICE.fail, { cut: true });
my.showToast({ content: err.message || '照片处理失败,请重新拍摄', type: 'none', duration: 3000 });
}).then(function () {
that.submitting = false;
if (signal.canceled && that.alive && that.visible && that.data.analyzing) that.submitPhotos();
});
},
onRetryAnalysis: function () { if (!this.busy) this.submitPhotos(); },
onScanError: function () { this.setData({ scanError: true }); },
onRestart: function () {
if (this.debugMode && !this.debugGateEnabled()) return;
if (this.signal) this.signal.canceled = true;
if (!this.debugMode && typeof api.discardAnalysis === 'function') api.discardAnalysis(this.attempt);
this.stopNativeFrames();
this.seq++; this.photos = []; this.preparedPhotos = []; this.reportPhotos = []; this.attempt = null; this.busy = false;
this.setData({ stepIndex: 0, previewUrl: '', analyzing: false, analysisError: '', doneTip: '', cameraEnabled: false });
this.renderStep(); this.mountCamera();
},
onDebugRestart: function () {
if (!this.debugMode || !this.debugGateEnabled()) return;
if (this.signal) this.signal.canceled = true;
this.stopNativeFrames(); clearTimeout(this.captureTimer); clearTimeout(this.nextTimer);
this.photos = []; this.preparedPhotos = []; this.reportPhotos = []; this.debugResults = []; this.pendingChoice = null;
this.choiceSeq = (this.choiceSeq || 0) + 1; this.busy = false; this.seq++;
this.probeFramesProcessed = 0; this.probeAutoCaptured = 0; this.probeFrame = null; this.probeDetection = null;
this.nativeFrameCallbacks = 0; this.nativeFramesSkippedInFlight = 0; this.nativeAcceptedCalls = 0; this.probeTimings = null;
this.probeStage = 'initializing'; this.probeFailureStage = 'none'; this.probeFailureCode = 'none';
this.setData({ debugComplete: false, debugPhotos: [], stepIndex: 0, previewUrl: '', analyzing: false, analysisError: '', doneTip: '', cameraEnabled: false, diagText: '' });
this.renderStep(); this.onEnableCamera();
},
onCancel: function () {
if (this.debugMode) { this.photos = []; this.preparedPhotos = []; this.reportPhotos = []; this.debugResults = []; this.setData({ debugPhotos: [], previewUrl: '', diagText: '' }); }
this.pause(); my.navigateBack();
},
noop: function () {}
}, typeof my === 'undefined' ? {} : my));