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) { // 淘宝 组件不可用:直接用系统相机/相册,页面不再挂相机组件(否则整页崩溃) 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));