Checkpoint: reclassifyWithAI traite désormais NULL + rules, supprime les non pertinents et conserve les tombstones
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"version": "2bfe4358",
|
"version": "aa6b358c",
|
||||||
"timestamp": 1783252262810
|
"timestamp": 1783341062192
|
||||||
}
|
}
|
||||||
@@ -37,7 +37,7 @@ import { scheduleDailyImport } from "./_core/index";
|
|||||||
import { loginLocalUser, hashPassword, ensureAdminExists } from "./localAuth";
|
import { loginLocalUser, hashPassword, ensureAdminExists } from "./localAuth";
|
||||||
import { classifyArticle } from "./aiClassifier";
|
import { classifyArticle } from "./aiClassifier";
|
||||||
import { getDb } from "./db";
|
import { getDb } from "./db";
|
||||||
import { veilleItems, aapItems, articleReads } from "../drizzle/schema";
|
import { veilleItems, aapItems, articleReads, processedDedupKeys } from "../drizzle/schema";
|
||||||
import { isNull, or, eq as eqDrizzle, and, inArray, count } from "drizzle-orm";
|
import { isNull, or, eq as eqDrizzle, and, inArray, count } from "drizzle-orm";
|
||||||
|
|
||||||
// ─── Middleware admin ─────────────────────────────────────────────────────────
|
// ─── Middleware admin ─────────────────────────────────────────────────────────
|
||||||
@@ -116,14 +116,15 @@ export const appRouter = router({
|
|||||||
const db = await getDb();
|
const db = await getDb();
|
||||||
if (!db) throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "DB indisponible" });
|
if (!db) throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "DB indisponible" });
|
||||||
|
|
||||||
// Récupérer les articles non encore classés par l'IA
|
// Récupérer les articles non encore classés par l'IA OU classés par le fallback rules
|
||||||
const rows = await db
|
const rows = await db
|
||||||
.select()
|
.select()
|
||||||
.from(veilleItems)
|
.from(veilleItems)
|
||||||
.where(or(isNull(veilleItems.iaClassifiedBy), isNull(veilleItems.iaRelevant)))
|
.where(or(isNull(veilleItems.iaClassifiedBy), isNull(veilleItems.iaRelevant), eqDrizzle(veilleItems.iaClassifiedBy, "rules")))
|
||||||
.limit(input.limit);
|
.limit(input.limit);
|
||||||
|
|
||||||
let processed = 0;
|
let processed = 0;
|
||||||
|
let deleted = 0;
|
||||||
let errors = 0;
|
let errors = 0;
|
||||||
|
|
||||||
for (const row of rows) {
|
for (const row of rows) {
|
||||||
@@ -133,22 +134,34 @@ export const appRouter = router({
|
|||||||
row.resume || "",
|
row.resume || "",
|
||||||
() => ({ typeVeille: (row.typeVeille || "informationnelle") as "reglementaire" | "concurrentielle" | "technologique" | "informationnelle" })
|
() => ({ typeVeille: (row.typeVeille || "informationnelle") as "reglementaire" | "concurrentielle" | "technologique" | "informationnelle" })
|
||||||
);
|
);
|
||||||
await db
|
if (!aiResult.relevant) {
|
||||||
.update(veilleItems)
|
// Supprimer l'article non pertinent
|
||||||
.set({
|
await db.delete(veilleItems).where(eqDrizzle(veilleItems.id, row.id));
|
||||||
typeVeille: (aiResult.typeVeille ?? row.typeVeille ?? "informationnelle") as any,
|
// Conserver le tombstone pour éviter la réinsertion
|
||||||
iaRelevant: aiResult.relevant,
|
if (row.dedupKey) {
|
||||||
iaClassifiedBy: aiResult.classifiedBy,
|
await db.insert(processedDedupKeys)
|
||||||
iaReason: aiResult.reason,
|
.values({ dedupKey: row.dedupKey, feedType: "veille" })
|
||||||
})
|
.onDuplicateKeyUpdate({ set: { dedupKey: row.dedupKey } });
|
||||||
.where(eqDrizzle(veilleItems.id, row.id));
|
}
|
||||||
processed++;
|
deleted++;
|
||||||
|
} else {
|
||||||
|
await db
|
||||||
|
.update(veilleItems)
|
||||||
|
.set({
|
||||||
|
typeVeille: (aiResult.typeVeille ?? row.typeVeille ?? "informationnelle") as any,
|
||||||
|
iaRelevant: aiResult.relevant,
|
||||||
|
iaClassifiedBy: aiResult.classifiedBy,
|
||||||
|
iaReason: aiResult.reason,
|
||||||
|
})
|
||||||
|
.where(eqDrizzle(veilleItems.id, row.id));
|
||||||
|
processed++;
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
errors++;
|
errors++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { processed, errors, total: rows.length };
|
return { processed, deleted, errors, total: rows.length };
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// ─── Marquage lu/non lu ──────────────────────────────────────────────────────────────────────
|
// ─── Marquage lu/non lu ──────────────────────────────────────────────────────────────────────
|
||||||
@@ -240,34 +253,45 @@ export const appRouter = router({
|
|||||||
const rows = await db
|
const rows = await db
|
||||||
.select()
|
.select()
|
||||||
.from(aapItems)
|
.from(aapItems)
|
||||||
.where(or(isNull(aapItems.iaClassifiedBy), isNull(aapItems.iaRelevant)))
|
.where(or(isNull(aapItems.iaClassifiedBy), isNull(aapItems.iaRelevant), eqDrizzle(aapItems.iaClassifiedBy, "rules")))
|
||||||
.limit(input.limit);
|
.limit(input.limit);
|
||||||
|
|
||||||
let processed = 0;
|
let processed = 0;
|
||||||
|
let deleted = 0;
|
||||||
let errors = 0;
|
let errors = 0;
|
||||||
|
|
||||||
for (const row of rows) {
|
for (const row of rows) {
|
||||||
try {
|
try {
|
||||||
const aiResult = await classifyArticle(
|
const aiResult = await classifyArticle(
|
||||||
row.titre || "",
|
row.titre || "",
|
||||||
"",
|
row.resume || "",
|
||||||
() => ({ typeVeille: "informationnelle" as const })
|
() => ({ typeVeille: "informationnelle" as const })
|
||||||
);
|
);
|
||||||
await db
|
if (!aiResult.relevant) {
|
||||||
.update(aapItems)
|
await db.delete(aapItems).where(eqDrizzle(aapItems.id, row.id));
|
||||||
.set({
|
if (row.dedupKey) {
|
||||||
iaRelevant: aiResult.relevant,
|
await db.insert(processedDedupKeys)
|
||||||
iaClassifiedBy: aiResult.classifiedBy,
|
.values({ dedupKey: row.dedupKey, feedType: "aap" })
|
||||||
iaReason: aiResult.reason,
|
.onDuplicateKeyUpdate({ set: { dedupKey: row.dedupKey } });
|
||||||
})
|
}
|
||||||
.where(eqDrizzle(aapItems.id, row.id));
|
deleted++;
|
||||||
processed++;
|
} else {
|
||||||
|
await db
|
||||||
|
.update(aapItems)
|
||||||
|
.set({
|
||||||
|
iaRelevant: aiResult.relevant,
|
||||||
|
iaClassifiedBy: aiResult.classifiedBy,
|
||||||
|
iaReason: aiResult.reason,
|
||||||
|
})
|
||||||
|
.where(eqDrizzle(aapItems.id, row.id));
|
||||||
|
processed++;
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
errors++;
|
errors++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { processed, errors, total: rows.length };
|
return { processed, deleted, errors, total: rows.length };
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// ─── Marquage lu/non lu AAP ──────────────────────────────────────────────────────────────────────
|
// ─── Marquage lu/non lu AAP ──────────────────────────────────────────────────────────────────────
|
||||||
|
|||||||
4
todo.md
4
todo.md
@@ -187,5 +187,5 @@
|
|||||||
- [x] Afficher la prochaine date d'exécution dynamique dans les Paramètres (selon mode/heure/jour choisi)
|
- [x] Afficher la prochaine date d'exécution dynamique dans les Paramètres (selon mode/heure/jour choisi)
|
||||||
- [x] Ajouter la règle de rétention configurable dans les Paramètres (supprimer articles > N mois)
|
- [x] Ajouter la règle de rétention configurable dans les Paramètres (supprimer articles > N mois)
|
||||||
- [x] Implémenter la purge automatique au démarrage du serveur selon la règle de rétention
|
- [x] Implémenter la purge automatique au démarrage du serveur selon la règle de rétention
|
||||||
- [ ] Déployer en recette
|
- [x] Déployer en recette
|
||||||
- [ ] Répliquer BDD recette vers production et déployer le code en production
|
- [x] Répliquer BDD recette vers production et déployer le code en production
|
||||||
|
|||||||
Reference in New Issue
Block a user