WalleGriffkinder commited on
Commit
8c8d431
·
verified ·
1 Parent(s): 80fdd13

Update server.js

Browse files
Files changed (1) hide show
  1. server.js +75 -342
server.js CHANGED
@@ -1,351 +1,84 @@
1
  // server.js
2
- const express = require('express');
3
- const { v4: uuidv4 } = require('uuid');
4
- const { createWorker, PSM } = require('tesseract.js');
5
- const genericPool = require('generic-pool');
6
- const path = require('path'); // Для langPath
7
-
8
- const app = express();
9
- const port = process.env.PORT || 7860;
10
-
11
- app.use(express.json({ limit: '5mb' }));
12
-
13
- const challengeStore = {};
14
- const CHALLENGE_EXPIRY_MS = 5 * 60 * 1000;
15
-
16
- const OCR_CHAR_SET = "ABCDEFGHJKMNPQRSTUVWXYZ2345689";
17
- const OCR_CHALLENGE_LENGTH = 6;
18
- const WEBGL_CHALLENGE_COLORS = [
19
- { name: "red", rgb: [1.0, 0.0, 0.0] }, { name: "green", rgb: [0.0, 1.0, 0.0] },
20
- { name: "blue", rgb: [0.0, 0.0, 1.0] }, { name: "yellow", rgb: [1.0, 1.0, 0.0] }
21
- ];
22
-
23
- const XOR_KEY = "KluchevoyeSlovoDlyaDemo";
24
-
25
- // --- Tesseract.js Worker Pool ---
26
- const TESSERACT_POOL_MIN = 2; // Минимальное количество воркеров
27
- const TESSERACT_POOL_MAX = 5; // Максимальное (зависит от CPU cores)
28
- let tesseractPool = null;
29
-
30
- const factory = {
31
- create: async () => {
32
- console.log(`[Pool] Creating new Tesseract worker...`);
33
- const worker = await createWorker('eng', 1, {
34
- // ВАЖНО: Укажите правильный путь к вашим языковым данным!
35
- // langPath: path.join(__dirname, 'tessdata'), // Если tessdata в корне проекта
36
- // gzip: true, // Если файлы .traineddata.gz
37
- cacheMethod: 'none', // Для stateless воркеров в пуле
38
- // logger: m => console.log(`[Worker ${worker?.id?.slice(0,5) || 'N/A'}] ${m.status} ${m.progress || ''}`)
39
- });
40
- await worker.setParameters({
41
- tessedit_char_whitelist: OCR_CHAR_SET,
42
- tessedit_pageseg_mode: PSM.SINGLE_WORD, // PSM.SINGLE_WORD = 8
43
- });
44
- console.log(`[Pool] Tesseract worker created.`);
45
- return worker;
46
- },
47
- destroy: async (worker) => {
48
- console.log(`[Pool] Terminating Tesseract worker...`);
49
- await worker.terminate();
50
- console.log(`[Pool] Tesseract worker terminated.`);
51
- }
52
- };
53
-
54
- const poolOptions = {
55
- min: TESSERACT_POOL_MIN,
56
- max: TESSERACT_POOL_MAX,
57
- acquireTimeoutMillis: 5000, // Таймаут на получение воркера из пула (5 секунд)
58
- // evictionRunIntervalMillis: 10000, // Как часто проверять неиспользуемые воркеры
59
- // numTestsPerEvictionRun: 3,
60
- // idleTimeoutMillis: 30000, // Время жизни неиспользуемого воркера
61
- };
62
-
63
- async function initializeTesseractPool() {
64
- try {
65
- tesseractPool = genericPool.createPool(factory, poolOptions);
66
- console.log(`Tesseract.js worker pool initialized (min: ${TESSERACT_POOL_MIN}, max: ${TESSERACT_POOL_MAX}).`);
67
- // "Прогрев" пула, создаем минимальное количество воркеров
68
- const promises = [];
69
- for (let i = 0; i < TESSERACT_POOL_MIN; i++) {
70
- promises.push(tesseractPool.acquire());
71
- }
72
- const workers = await Promise.all(promises);
73
- workers.forEach(worker => tesseractPool.release(worker));
74
- console.log(`${TESSERACT_POOL_MIN} workers pre-warmed and released.`);
75
-
76
- } catch (error) {
77
- console.error("Failed to initialize Tesseract.js worker pool:", error);
78
- process.exit(1); // Критическая ошибка, выходим
79
- }
80
- }
81
- initializeTesseractPool();
82
-
83
-
84
- async function recognizeWithPooledWorker(imageBuffer) {
85
- if (!tesseractPool) {
86
- console.error("[OCR] Tesseract pool not available!");
87
- return null;
88
- }
89
- let worker = null;
90
- try {
91
- worker = await tesseractPool.acquire();
92
- const { data: { text } } = await worker.recognize(imageBuffer);
93
- return text.trim().replace(new RegExp(`[^${OCR_CHAR_SET}]`, 'g'), '');
94
- } catch (error) {
95
- console.error("[OCR] Error during recognition with pooled worker:", error.message.slice(0,150));
96
- if (error.message.includes('ResourceRequest timed out')) { // Ошибка таймаута получения воркера
97
- console.error("[OCR] Could not acquire worker from pool in time.");
98
- }
99
- return null;
100
- } finally {
101
- if (worker) {
102
- await tesseractPool.release(worker);
103
- }
104
- }
105
- }
106
-
107
- async function performDualOCR(imageDataUrl) {
108
- if (!imageDataUrl || !imageDataUrl.startsWith('data:image/png;base64,')) return { text1: null, text2: null, success: false };
109
-
110
- const base64Data = imageDataUrl.replace(/^data:image\/png;base64,/, "");
111
- const imageBuffer = Buffer.from(base64Data, 'base64');
112
-
113
- // Пытаемся выполнить два распознавания параллельно
114
- const results = await Promise.allSettled([
115
- recognizeWithPooledWorker(imageBuffer),
116
- recognizeWithPooledWorker(imageBuffer)
117
- ]);
118
-
119
- const text1 = results[0].status === 'fulfilled' ? results[0].value : null;
120
- const text2 = results[1].status === 'fulfilled' ? results[1].value : null;
121
-
122
- if (results[0].status === 'rejected') console.warn("[DualOCR] OCR attempt 1 failed:", results[0].reason?.message.slice(0,100));
123
- if (results[1].status === 'rejected') console.warn("[DualOCR] OCR attempt 2 failed:", results[1].reason?.message.slice(0,100));
124
-
125
- // Если хотя бы один успешен и не null
126
- const successfulRecognition = text1 !== null ? text1 : (text2 !== null ? text2 : null);
127
-
128
- return {
129
- finalText: successfulRecognition, // Текст, который будем использовать для сравнения
130
- text1: text1, // Для логов
131
- text2: text2, // Для логов
132
- anySuccess: successfulRecognition !== null // Был ли хотя бы один успешный результат
133
- };
134
- }
135
-
136
-
137
- function xorStr(text, key) { /* ... (без изменений) ... */
138
- let r = "";
139
- for (let i = 0; i < text.length; i++) {
140
- r += String.fromCharCode(text.charCodeAt(i) ^ key.charCodeAt(i % key.length));
141
- }
142
- return r;
143
- }
144
- function decF(encodedData) { /* ... (без изменений) ... */
145
- try {
146
- const xored = atob(encodedData);
147
- const originalText = xorStr(xored, XOR_KEY);
148
- return JSON.parse(originalText);
149
- } catch (e) {
150
- console.error("Payload decode error:", e.message.slice(0,100));
151
- return null;
152
- }
153
- }
154
-
155
- app.use((req, res, next) => { /* ... (CORS без изменений) ... */
156
- res.header('Access-Control-Allow-Origin', '*');
157
- res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept');
158
- res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
159
- if (req.method === 'OPTIONS') return res.sendStatus(200);
160
- next();
161
- });
162
-
163
- app.get('/api/challenge', (req, res) => { /* ... (без изменений) ... */
164
- const sT = uuidv4();
165
- let oCT = "";
166
- for (let i = 0; i < OCR_CHALLENGE_LENGTH; i++) {
167
- oCT += OCR_CHAR_SET[Math.floor(Math.random() * OCR_CHAR_SET.length)];
168
- }
169
- const wCCInfo = WEBGL_CHALLENGE_COLORS[Math.floor(Math.random() * WEBGL_CHALLENGE_COLORS.length)];
170
- challengeStore[sT] = {
171
- iat: Date.now(), exp: Date.now() + CHALLENGE_EXPIRY_MS, usd: false, ip: req.ip,
172
- eOT: oCT, eWCN: wCCInfo.name, eWCR: wCCInfo.rgb
173
- };
174
- res.json({ sT, oCT, wCC: wCCInfo.rgb });
175
- });
176
 
177
  app.post('/api/check', async (req, res) => {
178
- const payload = req.body;
179
- let currentScore = 0;
180
- const stages = [];
181
- let mandatoryFail = false;
182
-
183
- const sT = payload.sT;
184
- const oCDU = payload.oCDU;
185
- const decData = payload.eD ? decF(payload.eD) : null;
186
-
187
- let stage0Passed = false;
188
- if (decData) { stage0Passed = true; currentScore += 10; }
189
- else { mandatoryFail = true; }
190
- stages.push({ n: "Этап 0 (Данные)", p: stage0Passed, pts: stage0Passed ? 10 : -100 });
191
-
192
- const wglR = decData?.wgl;
193
- const navI = decData?.nav;
194
- const perfT = decData?.perf;
195
- const autV = decData?.aut;
196
-
197
- let sValid = false; let sDet = null;
198
- let stage1Passed = false;
199
- if (!mandatoryFail) {
200
- if (!sT || !challengeStore[sT] || challengeStore[sT].usd || Date.now() > challengeStore[sT].exp) {
201
- mandatoryFail = true;
202
- } else { sDet = challengeStore[sT]; sValid = true; stage1Passed = true; currentScore += 30; }
203
- }
204
- stages.push({ n: "Этап 1 (Сессия)", p: stage1Passed, pts: stage1Passed ? 30 : (mandatoryFail && !stage0Passed ? 0 : -100) });
 
 
 
 
 
 
 
 
 
 
 
 
 
205
 
206
- let stage2Passed = false; let ocrActualPass = false;
207
- let ocrPoints = 0;
208
- if (!mandatoryFail) {
209
- if (!oCDU || !oCDU.startsWith('data:image/png;base64,') || oCDU.length < 200) {
210
- ocrPoints = -80; // Сильный штраф за отсутствие/невалидность OCR Canvas
211
- mandatoryFail = true;
 
212
  } else {
213
- const ocrResult = await performDualOCR(oCDU); // Используем двойной OCR
214
- const recognizedText = ocrResult.finalText;
215
- const expectedText = sDet.eOT;
216
-
217
- console.log(`[DualOCR] Expected: "${expectedText}", Attempt1: "${ocrResult.text1}", Attempt2: "${ocrResult.text2}", Final: "${recognizedText}"`);
218
-
219
- if (ocrResult.anySuccess) { // Если хотя бы одно распознавание вернуло не null
220
- if (recognizedText === expectedText) {
221
- ocrActualPass = true;
222
- ocrPoints = 40;
223
- } else {
224
- ocrPoints = -80; // Распознано, но неверно - это хуже, чем не распознано
225
- mandatoryFail = true;
226
- }
227
- } else { // Оба OCR провалились (например, таймаут пула или ошибка tesseract)
228
- ocrPoints = -70; // Штраф за полный провал OCR
229
- mandatoryFail = true;
230
- }
231
- }
232
-
233
- if (ocrActualPass) {
234
- stage2Passed = true;
235
- } else if (!mandatoryFail) { // Если не было явного провала выше, но OCR не прошел
236
- if (Math.random() < 0.10) { // 10% шанс "простить"
237
- stage2Passed = true;
238
- ocrPoints = 5; // Утешительный приз
239
- stages.push({ n: "Этап 2 (OCR)", p: true, pts: ocrPoints, note: "Прощено случайно" });
240
- } else {
241
- mandatoryFail = true;
242
- // ocrPoints уже должен быть отрицательным из логики выше
243
- stages.push({ n: "Этап 2 (OCR)", p: false, pts: ocrPoints });
244
- }
245
- }
246
- if (ocrActualPass) {
247
- stages.push({ n: "Этап 2 (OCR)", p: true, pts: ocrPoints });
248
- } else if (!stage2Passed && !stages.find(s => s.n === "Этап 2 (OCR)")) { // Если не прощено и еще не добавлено
249
- stages.push({ n: "Этап 2 (OCR)", p: false, pts: ocrPoints });
250
  }
251
- currentScore += ocrPoints;
252
-
253
- } else {
254
- stages.push({ n: "Этап 2 (OCR)", p: false, pts: 0 });
255
  }
256
-
257
- let webglBonus = 0; let webglStagePassed = false;
258
- if (sValid && wglR) {
259
- const expRGB = sDet.eWCR;
260
- const expPx = [Math.round(expRGB[0]*255), Math.round(expRGB[1]*255), Math.round(expRGB[2]*255)];
261
- if (wglR.px && wglR.px.length >= 3) {
262
- const cliPx = wglR.px.slice(0,3);
263
- if (cliPx.every((v, i) => Math.abs(v - expPx[i]) <= 10)) {
264
- webglBonus += 70; webglStagePassed = true;
265
- } else { webglBonus -= 10; }
266
- } else { webglBonus -= 5; }
267
- const rdr = wglR.rdr?.toLowerCase() || "";
268
- if (rdr.includes("swiftshader") || rdr.includes("llvmpipe")) {
269
- webglBonus -= 20; webglStagePassed = false;
270
- }
271
- } else if (wglR?.err) { webglBonus -= 5; }
272
- currentScore += webglBonus;
273
- stages.push({ n: "Этап 3 (WebGL)", p: webglStagePassed, pts: webglBonus });
274
-
275
- let navScore = 0; let perfScore = 0; let autoScore = 0;
276
- let navPassed = true; let perfPassed = true; let autoPassed = true;
277
-
278
- if (navI) {
279
- if (navI.wd === true) { navScore -= 30; navPassed = false; }
280
- if (!navI.ua || navI.ua === "") { navScore -= 10; navPassed = false; }
281
- else if (navI.ua.toLowerCase().includes("bot") || navI.ua.toLowerCase().includes("headless")) {
282
- if (!navI.ua.toLowerCase().includes("headlesschrome")) { navScore -= 20; navPassed = false; }
283
- }
284
- } else { navScore -= 5; navPassed = false; }
285
- currentScore += navScore;
286
- stages.push({ n: "Этап 4 (Браузер)", p: navPassed, pts: navScore });
287
-
288
- if (perfT && sValid) {
289
- const { dct, ocrt, wrt } = perfT;
290
- if (typeof dct === 'number' && dct < 10) { perfScore -= 10; perfPassed = false; }
291
- if (typeof ocrt === 'number' && ocrt < 3 && oCDU) { perfScore -= 10; perfPassed = false; }
292
- if (typeof wrt === 'number' && wrt < 3 && wglR?.px) { perfScore -= 10; perfPassed = false; }
293
- } else if (!perfT) { perfScore -= 3; perfPassed = false; }
294
- currentScore += perfScore;
295
- stages.push({ n: "Этап 5 (Произв-ть)", p: perfPassed, pts: perfScore });
296
-
297
- if (autV && Object.keys(autV).length > 0) { autoScore -= 40; autoPassed = false; }
298
- else { autoScore += 5; }
299
- currentScore += autoScore;
300
- stages.push({ n: "Этап 6 (Автоматиз.)", p: autoPassed, pts: autoScore });
301
-
302
- currentScore = Math.max(0, Math.min(currentScore, 150));
303
-
304
- let vT = ""; const finalAllowThreshold = 75;
305
-
306
- if (mandatoryFail) { vT = "Блокировать (Провал обязательной проверки)"; }
307
- else {
308
- if (!webglStagePassed && !navPassed && !perfPassed && !autoPassed && currentScore < finalAllowThreshold) { // Если WebGL не спас и все вторички плохи
309
- vT = "Блокировать (Множественные подозрения)";
310
- } else if (currentScore >= finalAllowThreshold) {
311
- vT = "Разрешено (Проверка пройдена)";
312
- if (sDet) sDet.usd = true;
313
- } else { vT = "Блокировать (Низкий балл доверия)"; }
314
- }
315
-
316
- console.log(`[${new Date().toISOString()}] Check: Token=${sT || 'N/A'}, Score=${currentScore}, Verdict=${vT}, IP=${req.ip}, MandFail=${mandatoryFail}`);
317
- res.json({ vT, fS: currentScore, cS: stages });
318
  });
319
 
320
- setInterval(() => { /* ... (очистка токенов как раньше) ... */
321
- const now = Date.now();
322
- for (const token in challengeStore) {
323
- if (challengeStore[token].exp < now || (challengeStore[token].usd && (now - challengeStore[token].iat > CHALLENGE_EXPIRY_MS * 2))) {
324
- delete challengeStore[token];
325
- }
326
- }
327
- }, 60 * 1000);
328
-
329
- // Graceful shutdown для пула
330
- async function shutdown() {
331
- console.log("Shutting down server...");
332
- if (tesseractPool) {
333
- console.log("Draining Tesseract pool...");
334
- await tesseractPool.drain().then(() => {
335
- console.log("Tesseract pool drained.");
336
- return tesseractPool.clear();
337
- }).then(() => {
338
- console.log("Tesseract pool cleared.");
339
- }).catch(err => {
340
- console.error("Error draining/clearing Tesseract pool:", err);
341
- });
342
- }
343
- process.exit(0);
344
- }
345
- process.on('SIGTERM', shutdown);
346
- process.on('SIGINT', shutdown);
347
-
348
-
349
- app.listen(port, () => {
350
- console.log(`Pooled OCR Browser check API server listening on port ${port}`);
351
- });
 
1
  // server.js
2
+ const express=require('express');const {v4:uuidv4}=require('uuid');const {createWorker,PSM}=require('tesseract.js');const genericPool=require('generic-pool');const path=require('path');const sharp=require('sharp');
3
+ const app=express();const port=process.env.PORT||7860;app.use(express.json({limit:'5mb'}));
4
+ const cs={};const CEX=5*60*1000;const OCS="ACEFHJKMNPRTUVWXY23469";const OCL=6;
5
+ const WCC=[{n:"r",rgb:[1,0,0]},{n:"g",rgb:[0,1,0]},{n:"b",rgb:[0,0,1]},{n:"y",rgb:[1,1,0]}];const XK="KluchevoyeSlovoDlyaDemo";
6
+ const TPL=4;let tp=null;let pDO=false;
7
+ const fac={create:async()=>{const w=await createWorker('eng',1,{langPath:path.join(__dirname,'tessdata'),gzip:true,cacheMethod:'none'});await w.setParameters({tessedit_char_whitelist:OCS,tessedit_pageseg_mode:PSM.SINGLE_WORD});return w;},destroy:async(w)=>{await w.terminate();}};
8
+ const pO={min:TPL,max:TPL,acquireTimeoutMillis:7000};
9
+ async function iTP(){try{tp=genericPool.createPool(fac,pO);console.log(`Pool(s:${TPL})OK`);const ws=await Promise.all(Array(TPL).fill(null).map(()=>tp.acquire()));ws.forEach(w=>tp.release(w));console.log(`${TPL}wOK`);}catch(e){console.error("PoolInitFail:",e);process.exit(1);}}
10
+ iTP();
11
+ async function pIFOCR(b){try{return await sharp(b).grayscale().normalize().sharpen({sigma:0.5,m1:0,m2:3,x1:0,y2:3,y3:3}).toBuffer();}catch(e){console.warn("PreProcFail:",e.message.slice(0,50));return b;}}
12
+ async function rWPW(b,h){if(!tp){console.error("PoolNA!");return null;}let w=null;try{w=await tp.acquire();const pB=await pIFOCR(b);const{data:{text}}=await w.recognize(pB);return text.trim().replace(new RegExp(`[^${OCS}]`,'g'),'');}catch(e){console.error(`OCR ${h} Err:`,e.message.slice(0,50));return null;}finally{if(w)await tp.release(w);}}
13
+ async function pSO(d){if(!d||!d.startsWith('data:image/png;base64,'))return{fT:null,aL:[]};const bD=d.replace(/^data:image\/png;base64,/,"");const iB=Buffer.from(bD,'base64');let rs=[];let aL=[];
14
+ if(!pDO&&tp&&tp.available>=2){pDO=true;try{const[r1,r2]=await Promise.all([rWPW(iB,"D1"),rWPW(iB,"D2")]);rs.push(r1,r2);aL.push({t:"D1",x:r1},{t:"D2",x:r2});}catch(e){console.error("DualOCRErr:",e);}finally{pDO=false;}}
15
+ else if(tp&&tp.available>=1){try{const rS=await rWPW(iB,"S1");rs.push(rS);aL.push({t:"S1",x:rS});}catch(e){console.error("SingleOCRErr:",e);}}
16
+ else{console.warn("NoWForOCR");aL.push({t:"N",x:null,r:"NoW"});}
17
+ const vR=rs.filter(t=>t!==null&&t!=="");const fT=vR.length>0?vR[0]:null;console.log(`OCRLog: ${aL.map(a=>`${a.t}:${a.x||'-'}`).join('; ')}. Fin:"${fT||'-'}"`);return{fT,aL};}
18
+ function ld(a,b){if(!a&&!b)return 0;if(!a)return b.length;if(!b)return a.length;const m=[];for(let i=0;i<=b.length;i++)m[i]=[i];for(let j=0;j<=a.length;j++)m[0][j]=j;for(let i=1;i<=b.length;i++){for(let j=1;j<=a.length;j++){if(b.charAt(i-1)===a.charAt(j-1))m[i][j]=m[i-1][j-1];else m[i][j]=Math.min(m[i-1][j-1]+1,Math.min(m[i][j-1]+1,m[i-1][j]+1));}}return m[b.length][a.length];}
19
+ function xS(t,k){let r="";for(let i=0;i<t.length;i++)r+=String.fromCharCode(t.charCodeAt(i)^k.charCodeAt(i%k.length));return r;}
20
+ function dF(d){try{const x=atob(d);const o=xS(x,XK);return JSON.parse(o);}catch(e){console.error("DecErr:",e.message.slice(0,50));return null;}}
21
+ app.use((req,res,next)=>{res.header('Access-Control-Allow-Origin','*');res.header('Access-Control-Allow-Headers','Origin,X-Requested-With,Content-Type,Accept');res.header('Access-Control-Allow-Methods','GET,POST,OPTIONS');if(req.method==='OPTIONS')return res.sendStatus(200);next();});
22
+ app.get('/api/challenge',(req,res)=>{const sT=uuidv4();let oCT="";for(let i=0;i<OCL;i++)oCT+=OCS[Math.floor(Math.random()*OCS.length)];const wI=WCC[Math.floor(Math.random()*WCC.length)];cs[sT]={iat:Date.now(),exp:Date.now()+CEX,usd:false,ip:req.ip,eOT:oCT,eWCN:wI.n,eWCR:wI.rgb};res.json({sT,oCT,wCC:wI.rgb});});
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
 
24
  app.post('/api/check', async (req, res) => {
25
+ const p=req.body; let sc=0; const stgs=[]; let mF=false;
26
+ const sT=p.sT; const oCDU=p.oCDU; const dD=p.eD?dF(p.eD):null;
27
+ let s0P=false; if(dD){s0P=true;sc+=10;}else{mF=true;} stgs.push({n:"E0",p:s0P,pts:s0P?10:-100});
28
+ const wR=dD?.wgl; const nI=dD?.nav; const pT=dD?.perf; const aV=dD?.aut;
29
+ let sV=false; let sDet=null; let s1P=false;
30
+ if(!mF){if(!sT||!cs[sT]||cs[sT].usd||Date.now()>cs[sT].exp){mF=true;}else{sDet=cs[sT];sV=true;s1P=true;sc+=30;}}
31
+ stgs.push({n:"E1",p:s1P,pts:s1P?30:(mF&&!s0P?0:-100)});
32
+
33
+ let ocrPassed=false; let ocrPts=0;
34
+ if(!mF){
35
+ if(!oCDU||!oCDU.startsWith('data:image/png;base64,')||oCDU.length<150){ocrPts=-80;mF=true;}
36
+ else{const oRes=await pSO(oCDU); const rT=oRes.fT; const eT=sDet.eOT;
37
+ if(rT!==null){const dist=ld(rT,eT);if(dist<=1){ocrPassed=true;ocrPts=40+(dist===0?5:0);}else{ocrPts=-80;mF=true;}}
38
+ else{ocrPts=-70;mF=true;}}
39
+ if(!ocrPassed&&!mF){if(Math.random()<0.10){ocrPassed=true;ocrPts=5;stgs.push({n:"E2",p:true,pts:ocrPts,note:"RND"});}else{mF=true;if(!stgs.find(s=>s.n==="E2"))stgs.push({n:"E2",p:false,pts:ocrPts});}}
40
+ if(ocrPassed&&!stgs.find(s=>s.n==="E2")){stgs.push({n:"E2",p:true,pts:ocrPts});}
41
+ else if(!ocrPassed&&!stgs.find(s=>s.n==="E2")){stgs.push({n:"E2",p:false,pts:ocrPts});}
42
+ sc+=ocrPts;
43
+ }else{stgs.push({n:"E2",p:false,pts:0});}
44
+
45
+ let webglPassed=false; let webglPts=0;
46
+ if(sV&&wR){const eR=sDet.eWCR;const eP=[Math.round(eR[0]*255),Math.round(eR[1]*255),Math.round(eR[2]*255)];
47
+ if(wR.px&&wR.px.length>=3){const cP=wR.px.slice(0,3);if(cP.every((v,i)=>Math.abs(v-eP[i])<=8)){webglPassed=true;webglPts=70;}else{webglPts=-10;}}else{webglPts=-5;}
48
+ const rdr=wR.rdr?.toLowerCase()||"";if(rdr.includes("swiftshader")||rdr.includes("llvmpipe")){webglPts-=20;webglPassed=false;}}else if(wR?.err){webglPts=-5;}
49
+ sc+=webglPts;stgs.push({n:"E3",p:webglPassed,pts:webglPts});
50
+
51
+ let navPassed=true; let navPts=0;
52
+ if(nI){if(nI.wd===true){navPts-=30;navPassed=false;}if(!nI.ua||nI.ua===""){navPts-=10;navPassed=false;}else if(nI.ua.toLowerCase().includes("bot")||nI.ua.toLowerCase().includes("headless")){if(!nI.ua.toLowerCase().includes("headlesschrome")){navPts-=20;navPassed=false;}}}else{navPts-=5;navPassed=false;}
53
+ sc+=navPts;stgs.push({n:"E4",p:navPassed,pts:navPts});
54
+
55
+ let perfPassed=true; let perfPts=0;
56
+ if(pT&&sV){const{dct,ocrt,wrt}=pT;if(typeof dct==='number'&&dct<10){perfPts-=10;perfPassed=false;}if(typeof ocrt==='number'&&ocrt<3&&oCDU){perfPts-=10;perfPassed=false;}if(typeof wrt==='number'&&wrt<3&&wR?.px){perfPts-=10;perfPassed=false;}}else if(!pT){perfPts-=3;perfPassed=false;}
57
+ sc+=perfPts;stgs.push({n:"E5",p:perfPassed,pts:perfPts});
58
+
59
+ let autoPassed=true; let autoPts=0;
60
+ if(aV&&Object.keys(aV).length>0){autoPts-=40;autoPassed=false;}else{autoPts+=5;}
61
+ sc+=autoPts;stgs.push({n:"E6",p:autoPassed,pts:autoPts});
62
+
63
+ sc=Math.max(0,Math.min(sc,150));
64
+ let vT="";
65
 
66
+ if(mF){vT="Блокировать (Критический провал)";}
67
+ else {
68
+ const primaryConditionMet = ocrPassed || webglPassed;
69
+ const secondaryConditionMet = navPassed || perfPassed || autoPassed;
70
+ if(primaryConditionMet && secondaryConditionMet){
71
+ vT="Разрешено";
72
+ if(sDet)sDet.usd=true;
73
  } else {
74
+ vT="Блокировать (Не пройдены условия)";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75
  }
 
 
 
 
76
  }
77
+ console.log(`[${new Date().toISOString().slice(0,19).replace('T',' ')}] Chk: T=${sT||'N/A'},S=${sc},V=${vT},IP=${req.ip},MF=${mF},OCR=${ocrPassed},WebGL=${webglPassed},Nav=${navPassed},Perf=${perfPassed},Auto=${autoPassed}`);
78
+ res.json({vT,fS:sc,cS:stgs});
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
  });
80
 
81
+ setInterval(()=>{const n=Date.now();for(const t in cs){if(cs[t].exp<n||(cs[t].usd&&(n-cs[t].iat>CEX*2))){delete cs[t];}}},60000);
82
+ async function shutdown(){console.log("Shutdown...");if(tp){console.log("Draining pool...");await tp.drain().then(()=>tp.clear()).then(()=>console.log("Pool cleared.")).catch(e=>console.error("Pool drain err:",e));}process.exit(0);}
83
+ process.on('SIGTERM',shutdown);process.on('SIGINT',shutdown);
84
+ app.listen(port,()=>{console.log(`RuleV3 API on ${port}`);});