@anonympins/fingerprint 0.2.1 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +56 -4
- package/fingerprint.client.js +3 -15
- package/fingerprint.js +161 -185
- package/library.js +83 -8
- package/mongodb-store.js +52 -52
- package/package.json +1 -1
- package/pow.solver.js +49 -13
- package/pow.worker.js +26 -26
- package/problem-manager.js +72 -0
- package/redis-store.js +42 -42
- package/sql-store.js +77 -77
package/library.js
CHANGED
|
@@ -1583,29 +1583,104 @@ Optimization.Operators.createFullSecurityConfigEvaluator = ({ trafficData }) =>
|
|
|
1583
1583
|
* @returns {Array<{solution: object, objectives: number[]}>} Le front de Pareto des configurations optimales.
|
|
1584
1584
|
*/
|
|
1585
1585
|
Optimization.Operators.solveFullSecurityTuning = (context, options = {}) => {
|
|
1586
|
-
const fitnessFunction = Optimization.Operators.createFullSecurityConfigEvaluator(context);
|
|
1586
|
+
const fitnessFunction = Optimization.Operators.createFullSecurityConfigEvaluator(context);
|
|
1587
1587
|
|
|
1588
1588
|
// Un "individu" est un objet de configuration complet
|
|
1589
1589
|
const createIndividual = () => ({
|
|
1590
1590
|
thresholds: {
|
|
1591
|
-
low:
|
|
1592
|
-
medium: 40 + Math.random() *
|
|
1593
|
-
high: 70 + Math.random() *
|
|
1591
|
+
low: 15 + Math.random() * 20, // 15-35
|
|
1592
|
+
medium: 40 + Math.random() * 25, // 40-65
|
|
1593
|
+
high: 70 + Math.random() * 20, // 70-90
|
|
1594
1594
|
},
|
|
1595
1595
|
weights: {
|
|
1596
1596
|
historyScore: Math.random(),
|
|
1597
1597
|
rotationScore: Math.random(),
|
|
1598
1598
|
headerAnomalyScore: Math.random(),
|
|
1599
|
-
requestPatternScore: Math.random(),
|
|
1599
|
+
requestPatternScore: 0.5 + Math.random(), // Donner plus d'importance aux patterns
|
|
1600
1600
|
inconsistencyScore: Math.random(),
|
|
1601
1601
|
honeypotScore: 1.0, // Garder le honeypot à 1.0 est une bonne pratique
|
|
1602
|
+
behaviorScore: Math.random(),
|
|
1603
|
+
crossLayerInconsistencyScore: Math.random(),
|
|
1604
|
+
timeInconsistencyScore: Math.random(),
|
|
1602
1605
|
},
|
|
1603
|
-
|
|
1606
|
+
patterns: {
|
|
1607
|
+
velocityThreshold: 100 + Math.random() * 400, // 100-500ms
|
|
1608
|
+
velocityWeight: 10 + Math.random() * 40,
|
|
1609
|
+
burstThreshold: 300 + Math.random() * 700, // 300-1000ms
|
|
1610
|
+
burstWeight: 20 + Math.random() * 40,
|
|
1611
|
+
scrapeThreshold: 500 + Math.random() * 1000, // 500-1500ms
|
|
1612
|
+
scrapeWeight: 15 + Math.random() * 35,
|
|
1613
|
+
sequenceLength: 3 + Math.floor(Math.random() * 3), // 3-5
|
|
1614
|
+
sequenceWeight: 20 + Math.random() * 50,
|
|
1615
|
+
regularityThreshold: 50 + Math.random() * 200, // 50-250ms
|
|
1616
|
+
regularityWeight: 20 + Math.random() * 40,
|
|
1617
|
+
decayFactor: 0.85 + Math.random() * 0.14, // 0.85-0.99
|
|
1618
|
+
inactivityReset: 15000 + Math.random() * 45000, // 15s-60s
|
|
1619
|
+
}
|
|
1604
1620
|
});
|
|
1605
1621
|
|
|
1606
1622
|
// Le crossover et la mutation doivent maintenant opérer sur des objets complexes
|
|
1607
|
-
const crossover = (c1, c2) => {
|
|
1608
|
-
|
|
1623
|
+
const crossover = (c1, c2) => {
|
|
1624
|
+
const child = JSON.parse(JSON.stringify(c1)); // Deep copy
|
|
1625
|
+
// Croisement pour chaque groupe de paramètres
|
|
1626
|
+
for (const key in child.thresholds) {
|
|
1627
|
+
child.thresholds[key] = (c1.thresholds[key] + c2.thresholds[key]) / 2;
|
|
1628
|
+
}
|
|
1629
|
+
for (const key in child.weights) {
|
|
1630
|
+
if (key !== 'honeypotScore') { // Ne pas croiser le poids du honeypot
|
|
1631
|
+
child.weights[key] = (c1.weights[key] + c2.weights[key]) / 2;
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
for (const key in child.patterns) {
|
|
1635
|
+
child.patterns[key] = (c1.patterns[key] + c2.patterns[key]) / 2;
|
|
1636
|
+
}
|
|
1637
|
+
return child;
|
|
1638
|
+
};
|
|
1639
|
+
|
|
1640
|
+
const mutate = (c) => {
|
|
1641
|
+
const newConfig = JSON.parse(JSON.stringify(c));
|
|
1642
|
+
|
|
1643
|
+
// --- NOUVELLE LOGIQUE : Sélection de section pondérée ---
|
|
1644
|
+
// On donne plus de poids à la mutation des 'patterns' et des 'weights',
|
|
1645
|
+
// car ils ont un impact plus direct sur la détection que les seuils.
|
|
1646
|
+
const sections = [
|
|
1647
|
+
{ name: 'patterns', weight: 0.5 }, // 50% de chance
|
|
1648
|
+
{ name: 'weights', weight: 0.35 }, // 35% de chance
|
|
1649
|
+
{ name: 'thresholds', weight: 0.15 } // 15% de chance
|
|
1650
|
+
];
|
|
1651
|
+
const rand = Math.random();
|
|
1652
|
+
let cumulativeWeight = 0;
|
|
1653
|
+
let sectionToMutate = 'patterns'; // Fallback
|
|
1654
|
+
for (const section of sections) {
|
|
1655
|
+
cumulativeWeight += section.weight;
|
|
1656
|
+
if (rand < cumulativeWeight) {
|
|
1657
|
+
sectionToMutate = section.name;
|
|
1658
|
+
break;
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
const keys = Object.keys(newConfig[sectionToMutate]);
|
|
1663
|
+
const keyToMutate = keys[Math.floor(Math.random() * keys.length)];
|
|
1664
|
+
|
|
1665
|
+
if (keyToMutate === 'honeypotScore') return newConfig; // Ne pas muter le poids du honeypot
|
|
1666
|
+
|
|
1667
|
+
// Appliquer une mutation avec une amplitude variable
|
|
1668
|
+
const mutationAmount = (Math.random() - 0.5) * 0.4; // +/- 20%
|
|
1669
|
+
newConfig[sectionToMutate][keyToMutate] *= (1 + mutationAmount);
|
|
1670
|
+
|
|
1671
|
+
// S'assurer que les valeurs restent dans des limites raisonnables
|
|
1672
|
+
if (sectionToMutate === 'weights') {
|
|
1673
|
+
newConfig[sectionToMutate][keyToMutate] = Math.max(0, Math.min(1.5, newConfig[sectionToMutate][keyToMutate]));
|
|
1674
|
+
}
|
|
1675
|
+
if (keyToMutate === 'decayFactor') {
|
|
1676
|
+
newConfig.patterns.decayFactor = Math.max(0.8, Math.min(0.999, newConfig.patterns.decayFactor));
|
|
1677
|
+
}
|
|
1678
|
+
if (keyToMutate.includes('Threshold') || keyToMutate.includes('Reset')) {
|
|
1679
|
+
newConfig.patterns[keyToMutate] = Math.max(50, newConfig.patterns[keyToMutate]);
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
return newConfig;
|
|
1683
|
+
};
|
|
1609
1684
|
|
|
1610
1685
|
return Optimization.geneticAlgorithmMultiObjective(
|
|
1611
1686
|
createIndividual,
|
package/mongodb-store.js
CHANGED
|
@@ -1,53 +1,53 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file Creates a store adapter for MongoDB.
|
|
3
|
-
* This adapter uses a collection as a key-value store and leverages MongoDB's TTL indexes
|
|
4
|
-
* for automatic expiration of documents.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Creates a store adapter for a MongoDB collection.
|
|
9
|
-
* It's recommended to pass the `db` object and let the adapter handle the collection.
|
|
10
|
-
*
|
|
11
|
-
* **Note:** For TTL to work, you must create a TTL index on the `expiresAt` field in your collection.
|
|
12
|
-
* In the mongo shell, run:
|
|
13
|
-
* `db.yourCollectionName.createIndex({ "expiresAt": 1 }, { expireAfterSeconds: 0 })`
|
|
14
|
-
*
|
|
15
|
-
* @param {import('mongodb').Db} db - An instance of a MongoDB Db object.
|
|
16
|
-
* @param {string} [collectionName='fingerprint_store'] - The name of the collection to use.
|
|
17
|
-
* @returns {import('./fingerprint.js').IStore} An object that complies with the IStore interface.
|
|
18
|
-
*/
|
|
19
|
-
export function createMongoDbStore(db, collectionName = 'fingerprint_store') {
|
|
20
|
-
const collection = db.collection(collectionName);
|
|
21
|
-
|
|
22
|
-
return {
|
|
23
|
-
async get(key) {
|
|
24
|
-
const doc = await collection.findOne({ _id: key });
|
|
25
|
-
// The TTL index automatically removes expired documents, so no need to check `expiresAt` here.
|
|
26
|
-
return doc ? doc.value : null;
|
|
27
|
-
},
|
|
28
|
-
async set(key, value, ttl) {
|
|
29
|
-
const doc = {
|
|
30
|
-
_id: key,
|
|
31
|
-
value: value,
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
if (ttl && ttl > 0) {
|
|
35
|
-
// Set the expiration date for the TTL index.
|
|
36
|
-
doc.expiresAt = new Date(Date.now() + ttl * 1000);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
await collection.updateOne(
|
|
40
|
-
{ _id: key },
|
|
41
|
-
{ $set: doc },
|
|
42
|
-
{ upsert: true }
|
|
43
|
-
);
|
|
44
|
-
},
|
|
45
|
-
async has(key) {
|
|
46
|
-
const count = await collection.countDocuments({ _id: key });
|
|
47
|
-
return count > 0;
|
|
48
|
-
},
|
|
49
|
-
async delete(key) {
|
|
50
|
-
await collection.deleteOne({ _id: key });
|
|
51
|
-
},
|
|
52
|
-
};
|
|
1
|
+
/**
|
|
2
|
+
* @file Creates a store adapter for MongoDB.
|
|
3
|
+
* This adapter uses a collection as a key-value store and leverages MongoDB's TTL indexes
|
|
4
|
+
* for automatic expiration of documents.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Creates a store adapter for a MongoDB collection.
|
|
9
|
+
* It's recommended to pass the `db` object and let the adapter handle the collection.
|
|
10
|
+
*
|
|
11
|
+
* **Note:** For TTL to work, you must create a TTL index on the `expiresAt` field in your collection.
|
|
12
|
+
* In the mongo shell, run:
|
|
13
|
+
* `db.yourCollectionName.createIndex({ "expiresAt": 1 }, { expireAfterSeconds: 0 })`
|
|
14
|
+
*
|
|
15
|
+
* @param {import('mongodb').Db} db - An instance of a MongoDB Db object.
|
|
16
|
+
* @param {string} [collectionName='fingerprint_store'] - The name of the collection to use.
|
|
17
|
+
* @returns {import('./fingerprint.js').IStore} An object that complies with the IStore interface.
|
|
18
|
+
*/
|
|
19
|
+
export function createMongoDbStore(db, collectionName = 'fingerprint_store') {
|
|
20
|
+
const collection = db.collection(collectionName);
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
async get(key) {
|
|
24
|
+
const doc = await collection.findOne({ _id: key });
|
|
25
|
+
// The TTL index automatically removes expired documents, so no need to check `expiresAt` here.
|
|
26
|
+
return doc ? doc.value : null;
|
|
27
|
+
},
|
|
28
|
+
async set(key, value, ttl) {
|
|
29
|
+
const doc = {
|
|
30
|
+
_id: key,
|
|
31
|
+
value: value,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
if (ttl && ttl > 0) {
|
|
35
|
+
// Set the expiration date for the TTL index.
|
|
36
|
+
doc.expiresAt = new Date(Date.now() + ttl * 1000);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
await collection.updateOne(
|
|
40
|
+
{ _id: key },
|
|
41
|
+
{ $set: doc },
|
|
42
|
+
{ upsert: true }
|
|
43
|
+
);
|
|
44
|
+
},
|
|
45
|
+
async has(key) {
|
|
46
|
+
const count = await collection.countDocuments({ _id: key });
|
|
47
|
+
return count > 0;
|
|
48
|
+
},
|
|
49
|
+
async delete(key) {
|
|
50
|
+
await collection.deleteOne({ _id: key });
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
53
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@anonympins/fingerprint",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "An HTTP(S) client mitigation and anti-bot protection library for Node.js/Express, based on digital fingerprinting and dynamic Proof-of-Work (PoW) challenges.",
|
|
5
5
|
"main": "fingerprint.js",
|
|
6
6
|
"type": "module",
|
package/pow.solver.js
CHANGED
|
@@ -339,14 +339,50 @@ export async function solveOptimizationTask(initialPopulation, generations) {
|
|
|
339
339
|
return population;
|
|
340
340
|
}
|
|
341
341
|
|
|
342
|
+
/**
|
|
343
|
+
* @class ChallengeSolution
|
|
344
|
+
* @description Encapsule une solution de challenge et fournit des méthodes pour la manipuler.
|
|
345
|
+
* @private
|
|
346
|
+
*/
|
|
347
|
+
class ChallengeSolution {
|
|
348
|
+
constructor(type, nonce, rawSolution) {
|
|
349
|
+
this.type = type;
|
|
350
|
+
this.nonce = nonce;
|
|
351
|
+
this.rawSolution = rawSolution;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Applique les paramètres de la solution à un objet URL.
|
|
356
|
+
* @param {URL} url - L'objet URL à modifier.
|
|
357
|
+
*/
|
|
358
|
+
applyToUrl(url) {
|
|
359
|
+
url.searchParams.set('pow_type', this.type);
|
|
360
|
+
url.searchParams.set('pow_nonce', this.nonce);
|
|
361
|
+
|
|
362
|
+
// Logique de formatage spécifique à chaque type de challenge
|
|
363
|
+
if (this.type === 'cpu_mem' || this.type === 'cpu_mem_inline' || this.type === 'cpu_target') {
|
|
364
|
+
Object.entries(this.rawSolution).forEach(([key, value]) => {
|
|
365
|
+
url.searchParams.set(`pow_solution_${key}`, String(value));
|
|
366
|
+
});
|
|
367
|
+
} else if (this.type === 'useful_work_task') {
|
|
368
|
+
url.searchParams.set('pow_solution_work_result', JSON.stringify(this.rawSolution.work_result));
|
|
369
|
+
url.searchParams.set('pow_problem_id', this.rawSolution.problem_id);
|
|
370
|
+
} else {
|
|
371
|
+
// Pour les cas simples comme 'tsp' où la solution est une seule valeur
|
|
372
|
+
url.searchParams.set('pow_solution', JSON.stringify(this.rawSolution));
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
342
377
|
/**
|
|
343
378
|
* Fonction principale qui reçoit un objet challenge et le résout.
|
|
344
379
|
* @param {object} challenge - L'objet challenge reçu du serveur.
|
|
345
|
-
* @
|
|
380
|
+
* @param {string} [fingerprint=''] - L'empreinte de l'appareil qui résout le challenge.
|
|
381
|
+
* @returns {Promise<ChallengeSolution>} Un objet `ChallengeSolution` encapsulant le résultat.
|
|
346
382
|
*/
|
|
347
383
|
export async function solveChallenge(challenge, fingerprint = '') { // The fingerprint is now passed from the client library
|
|
348
384
|
const { type, nonce, clientSecret, cpuTarget, memDifficulty, cities, clientIp, targetMaxDistance, optimizationTask, usefulWorkTask } = challenge;
|
|
349
|
-
|
|
385
|
+
let rawSolution = {};
|
|
350
386
|
|
|
351
387
|
switch (type) {
|
|
352
388
|
case 'cpu_target':
|
|
@@ -357,7 +393,7 @@ export async function solveChallenge(challenge, fingerprint = '') { // The finge
|
|
|
357
393
|
const target = cpuTarget; // Keep variable name for consistency below
|
|
358
394
|
// Pour ce challenge simple, le baseBlock est juste le nonce.
|
|
359
395
|
const baseBlockBytes = new TextEncoder().encode(nonce + ":");
|
|
360
|
-
|
|
396
|
+
rawSolution.cpu = await solveCpuTargetInline(baseBlockBytes, target, null);
|
|
361
397
|
break;
|
|
362
398
|
case 'cpu_mem':
|
|
363
399
|
// Pour les appels API, le client IP n'est pas connu, on ne le met pas dans le message
|
|
@@ -371,8 +407,8 @@ export async function solveChallenge(challenge, fingerprint = '') { // The finge
|
|
|
371
407
|
})(),
|
|
372
408
|
solveMemory(memSeed, memDifficulty)
|
|
373
409
|
]);
|
|
374
|
-
|
|
375
|
-
|
|
410
|
+
rawSolution.cpu = cpuSol;
|
|
411
|
+
rawSolution.mem = memSol;
|
|
376
412
|
break;
|
|
377
413
|
case 'cpu_mem_inline':
|
|
378
414
|
// Version inline pour compatibilité HTML avec IP incluse
|
|
@@ -385,28 +421,28 @@ export async function solveChallenge(challenge, fingerprint = '') { // The finge
|
|
|
385
421
|
})(),
|
|
386
422
|
solveMemory(memSeedInline, memDifficulty)
|
|
387
423
|
]);
|
|
388
|
-
|
|
389
|
-
|
|
424
|
+
rawSolution.cpu = cpuSolInline;
|
|
425
|
+
rawSolution.mem = memSolInline;
|
|
390
426
|
break;
|
|
391
427
|
case 'tsp':
|
|
392
428
|
const tspResult = await solveTsp(cities, targetMaxDistance);
|
|
393
|
-
|
|
394
|
-
|
|
429
|
+
// Pour ce challenge, la solution est juste le chemin.
|
|
430
|
+
rawSolution = tspResult.path;
|
|
395
431
|
break;
|
|
396
432
|
case 'optimization_task':
|
|
397
433
|
const finalPopulation = await solveOptimizationTask(optimizationTask.population, optimizationTask.generations);
|
|
398
|
-
|
|
434
|
+
rawSolution = finalPopulation.map(p => p.chromosome); // On ne renvoie que les chromosomes
|
|
399
435
|
break;
|
|
400
436
|
case 'useful_work_task':
|
|
401
437
|
const workResult = await solveUsefulWorkTask(usefulWorkTask.task);
|
|
402
|
-
|
|
403
|
-
|
|
438
|
+
rawSolution.work_result = workResult;
|
|
439
|
+
rawSolution.problem_id = usefulWorkTask.problemId;
|
|
404
440
|
break;
|
|
405
441
|
default:
|
|
406
442
|
throw new Error(`Unknown challenge type: ${type}`);
|
|
407
443
|
}
|
|
408
444
|
|
|
409
|
-
return
|
|
445
|
+
return new ChallengeSolution(type, nonce, rawSolution);
|
|
410
446
|
}
|
|
411
447
|
|
|
412
448
|
// --- Compatibilité pour l'injection directe dans le HTML ---
|
package/pow.worker.js
CHANGED
|
@@ -1,27 +1,27 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file @/pow.worker.js
|
|
3
|
-
* @description Web Worker dédié à la résolution du challenge CPU.
|
|
4
|
-
* Ce script s'exécute sur un thread séparé pour ne pas bloquer l'interface utilisateur.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
self.onmessage = async (event) => {
|
|
8
|
-
const { message, target } = event.data;
|
|
9
|
-
let solution = 0;
|
|
10
|
-
const encoder = new TextEncoder();
|
|
11
|
-
|
|
12
|
-
// La boucle de calcul intensive est isolée dans ce worker.
|
|
13
|
-
while (true) {
|
|
14
|
-
const currentMessage = `${message}:${solution}`;
|
|
15
|
-
const data = encoder.encode(currentMessage);
|
|
16
|
-
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
|
|
17
|
-
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
18
|
-
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
|
|
19
|
-
|
|
20
|
-
if (BigInt('0x' + hashHex) < target) {
|
|
21
|
-
// Une fois la solution trouvée, on la renvoie au thread principal.
|
|
22
|
-
self.postMessage({ solution });
|
|
23
|
-
return;
|
|
24
|
-
}
|
|
25
|
-
solution++;
|
|
26
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* @file @/pow.worker.js
|
|
3
|
+
* @description Web Worker dédié à la résolution du challenge CPU.
|
|
4
|
+
* Ce script s'exécute sur un thread séparé pour ne pas bloquer l'interface utilisateur.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
self.onmessage = async (event) => {
|
|
8
|
+
const { message, target } = event.data;
|
|
9
|
+
let solution = 0;
|
|
10
|
+
const encoder = new TextEncoder();
|
|
11
|
+
|
|
12
|
+
// La boucle de calcul intensive est isolée dans ce worker.
|
|
13
|
+
while (true) {
|
|
14
|
+
const currentMessage = `${message}:${solution}`;
|
|
15
|
+
const data = encoder.encode(currentMessage);
|
|
16
|
+
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
|
|
17
|
+
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
18
|
+
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
|
|
19
|
+
|
|
20
|
+
if (BigInt('0x' + hashHex) < target) {
|
|
21
|
+
// Une fois la solution trouvée, on la renvoie au thread principal.
|
|
22
|
+
self.postMessage({ solution });
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
solution++;
|
|
26
|
+
}
|
|
27
27
|
};
|
package/problem-manager.js
CHANGED
|
@@ -169,6 +169,78 @@ class ProblemManager {
|
|
|
169
169
|
}
|
|
170
170
|
this.saveProblems();
|
|
171
171
|
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* S'assure qu'un problème a une solution initiale. Si non, en génère une.
|
|
175
|
+
* @param {object} problem - L'objet problème.
|
|
176
|
+
* @private
|
|
177
|
+
*/
|
|
178
|
+
_ensureInitialSolution(problem) {
|
|
179
|
+
if (problem.state.bestSolution) {
|
|
180
|
+
return; // Une solution existe déjà
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
console.log(`[ProblemManager] Génération d'une solution initiale pour le problème ${problem.id}...`);
|
|
184
|
+
|
|
185
|
+
// On se base sur le type de problème pour générer une solution de base.
|
|
186
|
+
// Pour l'instant, on gère le cas le plus commun (TSP/points)
|
|
187
|
+
// qui utilise le recuit simulé.
|
|
188
|
+
switch (problem.workUnit.type) {
|
|
189
|
+
case 'simulated_annealing_iterations': {
|
|
190
|
+
// Pour un TSP, une solution initiale est un ordre des points.
|
|
191
|
+
// On prend l'ordre initial des points du payload.
|
|
192
|
+
const initialSolution = problem.payload.points;
|
|
193
|
+
if (initialSolution && Array.isArray(initialSolution)) {
|
|
194
|
+
// On calcule l'énergie (coût) de cette solution initiale.
|
|
195
|
+
const energy = Optimization.tsp.calculateEnergy(initialSolution);
|
|
196
|
+
problem.state.bestSolution = initialSolution;
|
|
197
|
+
problem.state.bestEnergy = energy;
|
|
198
|
+
problem.state.lastUpdate = new Date().toISOString();
|
|
199
|
+
console.log(`[ProblemManager] Solution initiale pour ${problem.id} générée avec une énergie de ${energy.toFixed(2)}.`);
|
|
200
|
+
this.saveProblems(); // On sauvegarde la nouvelle solution
|
|
201
|
+
}
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
// D'autres types de problèmes (ex: algo génétique) pourraient être ajoutés ici.
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Récupère la meilleure solution actuellement connue pour un ou plusieurs problèmes.
|
|
210
|
+
* @param {string} [problemId] - L'ID optionnel du problème à consulter.
|
|
211
|
+
* Si non fourni, retourne les meilleures solutions pour tous les problèmes.
|
|
212
|
+
* @returns {object|Array<object>|null}
|
|
213
|
+
* - Si un `problemId` est fourni, retourne un objet `{ id, solution, score }` ou `null` si non trouvé.
|
|
214
|
+
* - Si aucun `problemId` n'est fourni, retourne un tableau de ces objets.
|
|
215
|
+
*/
|
|
216
|
+
getBestSolutions(problemId) {
|
|
217
|
+
const problemsToProcess = problemId
|
|
218
|
+
? this.problems.filter(p => p.id === problemId)
|
|
219
|
+
: this.problems;
|
|
220
|
+
|
|
221
|
+
problemsToProcess.forEach(p => this._ensureInitialSolution(p));
|
|
222
|
+
|
|
223
|
+
const formatSolution = (p) => {
|
|
224
|
+
// Après _ensureInitialSolution, on peut supposer que p.state existe.
|
|
225
|
+
if (!p || !p.state) return null;
|
|
226
|
+
return {
|
|
227
|
+
id: p.id,
|
|
228
|
+
solution: p.state.bestSolution,
|
|
229
|
+
// Gère les deux types de scores : 'energy' (recuit simulé) et 'fitness' (algo génétique)
|
|
230
|
+
score: p.state.bestEnergy !== undefined ? p.state.bestEnergy : p.state.bestFitness,
|
|
231
|
+
lastUpdate: p.state.lastUpdate,
|
|
232
|
+
};
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
if (problemId) {
|
|
236
|
+
const problem = this.problems.find(p => p.id === problemId);
|
|
237
|
+
return problem ? formatSolution(problem) : null; // Le filtrage initial a déjà fait le travail
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Retourne un aperçu pour tous les problèmes
|
|
241
|
+
return this.problems.map(formatSolution).filter(s => s && s.solution);
|
|
242
|
+
}
|
|
243
|
+
|
|
172
244
|
}
|
|
173
245
|
|
|
174
246
|
export { ProblemManager }; // Export the class for testing
|
package/redis-store.js
CHANGED
|
@@ -1,43 +1,43 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file Creates a store adapter for ioredis.
|
|
3
|
-
* This adapter handles the serialization and deserialization of complex objects,
|
|
4
|
-
* including the conversion of Set objects to arrays for storage in Redis.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Creates a store adapter for an ioredis client.
|
|
9
|
-
* @param {import('ioredis').Redis} redisClient - An instance of the ioredis client.
|
|
10
|
-
* @returns {import('./fingerprint.js').IStore} An object that complies with the IStore interface.
|
|
11
|
-
*/
|
|
12
|
-
export function createRedisStore(redisClient) {
|
|
13
|
-
return {
|
|
14
|
-
async get(key) {
|
|
15
|
-
const value = await redisClient.get(key);
|
|
16
|
-
if (!value) return null;
|
|
17
|
-
// Use a reviver to convert arrays back to Sets for specific keys like 'ips'.
|
|
18
|
-
return JSON.parse(value, (k, v) => {
|
|
19
|
-
if (k === 'ips' && Array.isArray(v)) {
|
|
20
|
-
return new Set(v);
|
|
21
|
-
}
|
|
22
|
-
return v;
|
|
23
|
-
});
|
|
24
|
-
},
|
|
25
|
-
async set(key, value, ttl) {
|
|
26
|
-
// Use a replacer to convert Set objects into arrays before serialization.
|
|
27
|
-
const stringValue = JSON.stringify(value, (k, v) => {
|
|
28
|
-
if (v instanceof Set) {
|
|
29
|
-
return Array.from(v);
|
|
30
|
-
}
|
|
31
|
-
return v;
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
if (ttl && ttl > 0) {
|
|
35
|
-
await redisClient.set(key, stringValue, 'EX', ttl);
|
|
36
|
-
} else {
|
|
37
|
-
await redisClient.set(key, stringValue);
|
|
38
|
-
}
|
|
39
|
-
},
|
|
40
|
-
async has(key) { return (await redisClient.exists(key)) === 1; },
|
|
41
|
-
async delete(key) { await redisClient.del(key); },
|
|
42
|
-
};
|
|
1
|
+
/**
|
|
2
|
+
* @file Creates a store adapter for ioredis.
|
|
3
|
+
* This adapter handles the serialization and deserialization of complex objects,
|
|
4
|
+
* including the conversion of Set objects to arrays for storage in Redis.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Creates a store adapter for an ioredis client.
|
|
9
|
+
* @param {import('ioredis').Redis} redisClient - An instance of the ioredis client.
|
|
10
|
+
* @returns {import('./fingerprint.js').IStore} An object that complies with the IStore interface.
|
|
11
|
+
*/
|
|
12
|
+
export function createRedisStore(redisClient) {
|
|
13
|
+
return {
|
|
14
|
+
async get(key) {
|
|
15
|
+
const value = await redisClient.get(key);
|
|
16
|
+
if (!value) return null;
|
|
17
|
+
// Use a reviver to convert arrays back to Sets for specific keys like 'ips'.
|
|
18
|
+
return JSON.parse(value, (k, v) => {
|
|
19
|
+
if (k === 'ips' && Array.isArray(v)) {
|
|
20
|
+
return new Set(v);
|
|
21
|
+
}
|
|
22
|
+
return v;
|
|
23
|
+
});
|
|
24
|
+
},
|
|
25
|
+
async set(key, value, ttl) {
|
|
26
|
+
// Use a replacer to convert Set objects into arrays before serialization.
|
|
27
|
+
const stringValue = JSON.stringify(value, (k, v) => {
|
|
28
|
+
if (v instanceof Set) {
|
|
29
|
+
return Array.from(v);
|
|
30
|
+
}
|
|
31
|
+
return v;
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
if (ttl && ttl > 0) {
|
|
35
|
+
await redisClient.set(key, stringValue, 'EX', ttl);
|
|
36
|
+
} else {
|
|
37
|
+
await redisClient.set(key, stringValue);
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
async has(key) { return (await redisClient.exists(key)) === 1; },
|
|
41
|
+
async delete(key) { await redisClient.del(key); },
|
|
42
|
+
};
|
|
43
43
|
}
|