Checkpoint: reclassifyWithAI traite désormais NULL + rules, supprime les non pertinents et conserve les tombstones

This commit is contained in:
Manus
2026-07-06 08:31:02 -04:00
parent 55f3d5a5d4
commit 7261a7d33e
3 changed files with 54 additions and 30 deletions

View File

@@ -37,7 +37,7 @@ import { scheduleDailyImport } from "./_core/index";
import { loginLocalUser, hashPassword, ensureAdminExists } from "./localAuth";
import { classifyArticle } from "./aiClassifier";
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";
// ─── Middleware admin ─────────────────────────────────────────────────────────
@@ -116,14 +116,15 @@ export const appRouter = router({
const db = await getDb();
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
.select()
.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);
let processed = 0;
let deleted = 0;
let errors = 0;
for (const row of rows) {
@@ -133,22 +134,34 @@ export const appRouter = router({
row.resume || "",
() => ({ typeVeille: (row.typeVeille || "informationnelle") as "reglementaire" | "concurrentielle" | "technologique" | "informationnelle" })
);
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++;
if (!aiResult.relevant) {
// Supprimer l'article non pertinent
await db.delete(veilleItems).where(eqDrizzle(veilleItems.id, row.id));
// Conserver le tombstone pour éviter la réinsertion
if (row.dedupKey) {
await db.insert(processedDedupKeys)
.values({ dedupKey: row.dedupKey, feedType: "veille" })
.onDuplicateKeyUpdate({ set: { dedupKey: row.dedupKey } });
}
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 {
errors++;
}
}
return { processed, errors, total: rows.length };
return { processed, deleted, errors, total: rows.length };
}),
// ─── Marquage lu/non lu ──────────────────────────────────────────────────────────────────────
@@ -240,34 +253,45 @@ export const appRouter = router({
const rows = await db
.select()
.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);
let processed = 0;
let deleted = 0;
let errors = 0;
for (const row of rows) {
try {
const aiResult = await classifyArticle(
row.titre || "",
"",
row.resume || "",
() => ({ typeVeille: "informationnelle" as const })
);
await db
.update(aapItems)
.set({
iaRelevant: aiResult.relevant,
iaClassifiedBy: aiResult.classifiedBy,
iaReason: aiResult.reason,
})
.where(eqDrizzle(aapItems.id, row.id));
processed++;
if (!aiResult.relevant) {
await db.delete(aapItems).where(eqDrizzle(aapItems.id, row.id));
if (row.dedupKey) {
await db.insert(processedDedupKeys)
.values({ dedupKey: row.dedupKey, feedType: "aap" })
.onDuplicateKeyUpdate({ set: { dedupKey: row.dedupKey } });
}
deleted++;
} else {
await db
.update(aapItems)
.set({
iaRelevant: aiResult.relevant,
iaClassifiedBy: aiResult.classifiedBy,
iaReason: aiResult.reason,
})
.where(eqDrizzle(aapItems.id, row.id));
processed++;
}
} catch {
errors++;
}
}
return { processed, errors, total: rows.length };
return { processed, deleted, errors, total: rows.length };
}),
// ─── Marquage lu/non lu AAP ──────────────────────────────────────────────────────────────────────