feat(scanner): Ausschluss-Unternehmen je Suchagent
Neues Feld exclude_companies: steht ein Eintrag im Firmennamen der Anzeige (z. B. "Hays"), verwirft der Agent sie. Beim Speichern werden vorhandene Treffer dieser Firmen gelöscht (außer "in Bearbeitung"). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
a24fd938a2
commit
1969d0b77a
@@ -191,6 +191,12 @@ async function migrateColumns() {
|
||||
'alarm_keywords',
|
||||
'ALTER TABLE icon_scanner_agents ADD COLUMN alarm_keywords TEXT NULL AFTER excludes',
|
||||
],
|
||||
// Scanner: Ausschluss-Unternehmen je Agent ("Hays"), 20.10.2026.
|
||||
[
|
||||
'icon_scanner_agents',
|
||||
'exclude_companies',
|
||||
'ALTER TABLE icon_scanner_agents ADD COLUMN exclude_companies TEXT NULL AFTER alarm_keywords',
|
||||
],
|
||||
[
|
||||
'icon_scanner_matches',
|
||||
'alarm',
|
||||
|
||||
@@ -4,7 +4,7 @@ import { requireAdmin, requireAuth } from '../middleware/auth.js';
|
||||
import { logHistory } from '../history.js';
|
||||
import { SOURCES, SOURCE_IDS, sourceReady } from '../scanner/sources.js';
|
||||
import { parseList, runScan, scanStatus } from '../scanner/pipeline.js';
|
||||
import { stripQuotes } from '../scanner/matching.js';
|
||||
import { isExcludedCompany, stripQuotes } from '../scanner/matching.js';
|
||||
import {
|
||||
getLastScanAt,
|
||||
loadScannerSecrets,
|
||||
@@ -37,6 +37,7 @@ const agentOut = (row) => ({
|
||||
// Nur Quellen, die es noch gibt (Jooble ist am 29.09.2026 entfallen).
|
||||
sources: parseList(row.sources).filter((id) => SOURCE_IDS.includes(id)),
|
||||
excludes: parseList(row.excludes),
|
||||
exclude_companies: parseList(row.exclude_companies),
|
||||
alarm_keywords: parseList(row.alarm_keywords),
|
||||
is_active: Boolean(row.is_active),
|
||||
matches: Number(row.matches ?? 0),
|
||||
@@ -121,32 +122,54 @@ function readAgent(body) {
|
||||
const keywords = cleanList(body?.keywords);
|
||||
const sources = cleanList(body?.sources).filter((id) => SOURCE_IDS.includes(id));
|
||||
const excludes = cleanList(body?.excludes);
|
||||
const excludeCompanies = cleanList(body?.exclude_companies);
|
||||
const alarmKeywords = cleanList(body?.alarm_keywords);
|
||||
if (!name) return { error: 'Bitte einen Namen angeben.' };
|
||||
if (keywords.length === 0 && alarmKeywords.length === 0) {
|
||||
return { error: 'Mindestens ein Schlagwort oder Alarm-Schlagwort angeben.' };
|
||||
}
|
||||
if (sources.length === 0) return { error: 'Mindestens eine Börse auswählen.' };
|
||||
return { name, keywords, alarmKeywords, sources, excludes, is_active: body?.is_active !== false };
|
||||
return { name, keywords, alarmKeywords, sources, excludes, excludeCompanies, is_active: body?.is_active !== false };
|
||||
}
|
||||
|
||||
/** Spaltenwerte in der Reihenfolge name, keywords, alarm_keywords, sources, excludes, is_active. */
|
||||
/**
|
||||
* Spaltenwerte in der Reihenfolge name, keywords, alarm_keywords, sources,
|
||||
* excludes, exclude_companies, is_active.
|
||||
*/
|
||||
const agentValues = (agent) => [
|
||||
agent.name,
|
||||
JSON.stringify(agent.keywords),
|
||||
JSON.stringify(agent.alarmKeywords),
|
||||
JSON.stringify(agent.sources),
|
||||
JSON.stringify(agent.excludes),
|
||||
JSON.stringify(agent.excludeCompanies),
|
||||
agent.is_active ? 1 : 0,
|
||||
];
|
||||
|
||||
/**
|
||||
* Bereits gespeicherte Treffer von Ausschluss-Unternehmen entfernen - sonst
|
||||
* blieben sie bis zum Aufraeumen sichtbar. Treffer "in Bearbeitung" bleiben,
|
||||
* an denen arbeitet schon jemand.
|
||||
*/
|
||||
async function dropExcludedMatches(agentId, agent) {
|
||||
if (agent.excludeCompanies.length === 0) return;
|
||||
const [rows] = await pool.query(
|
||||
`SELECT m.id, j.company FROM icon_scanner_matches m JOIN icon_scanner_jobs j ON j.id = m.job_id
|
||||
WHERE m.agent_id = ? AND m.status <> 'in_bearbeitung'`,
|
||||
[agentId]
|
||||
);
|
||||
const ids = rows.filter((row) => isExcludedCompany(agent, row.company)).map((row) => row.id);
|
||||
if (ids.length > 0) await pool.query('DELETE FROM icon_scanner_matches WHERE id IN (?)', [ids]);
|
||||
}
|
||||
|
||||
router.post('/agents', async (req, res, next) => {
|
||||
const agent = readAgent(req.body);
|
||||
if (agent.error) return res.status(400).json({ error: agent.error });
|
||||
try {
|
||||
const [result] = await pool.query(
|
||||
`INSERT INTO icon_scanner_agents (name, keywords, alarm_keywords, sources, excludes, is_active, created_by)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
||||
`INSERT INTO icon_scanner_agents
|
||||
(name, keywords, alarm_keywords, sources, excludes, exclude_companies, is_active, created_by)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[...agentValues(agent), req.user.id]
|
||||
);
|
||||
await logHistory({
|
||||
@@ -169,11 +192,13 @@ router.put('/agents/:id', async (req, res, next) => {
|
||||
try {
|
||||
const [result] = await pool.query(
|
||||
`UPDATE icon_scanner_agents
|
||||
SET name = ?, keywords = ?, alarm_keywords = ?, sources = ?, excludes = ?, is_active = ?
|
||||
SET name = ?, keywords = ?, alarm_keywords = ?, sources = ?, excludes = ?, exclude_companies = ?,
|
||||
is_active = ?
|
||||
WHERE id = ?`,
|
||||
[...agentValues(agent), Number(req.params.id)]
|
||||
);
|
||||
if (result.affectedRows === 0) return res.status(404).json({ error: 'Suchagent nicht gefunden.' });
|
||||
await dropExcludedMatches(Number(req.params.id), agent);
|
||||
const [[row]] = await pool.query('SELECT * FROM icon_scanner_agents WHERE id = ?', [Number(req.params.id)]);
|
||||
res.json({ agent: agentOut(row) });
|
||||
} catch (err) {
|
||||
|
||||
@@ -54,6 +54,16 @@ export function agentTerms(agent) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Ausschluss-Unternehmen: geprueft wird nur der Firmenname der Anzeige, nicht
|
||||
* der Text - "Hays" soll Anzeigen von Hays ausblenden, nicht jede, die Hays
|
||||
* irgendwo erwaehnt.
|
||||
*/
|
||||
export function isExcludedCompany(agent, company) {
|
||||
if (!company) return false;
|
||||
return (agent.excludeCompanies ?? []).some((term) => containsTerm(company, term));
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {{ score: number, hits: string[], alarm: boolean } | null}
|
||||
* null = ausgeschlossen oder kein einziges Schlagwort gefunden.
|
||||
@@ -64,6 +74,7 @@ export function scoreJob(agent, job) {
|
||||
const all = `${title}\n${job.company ?? ''}\n${job.description ?? ''}`;
|
||||
|
||||
if ((agent.excludes ?? []).some((term) => containsTerm(all, term))) return null;
|
||||
if (isExcludedCompany(agent, job.company)) return null;
|
||||
|
||||
const keywords = agentTerms(agent);
|
||||
if (keywords.length === 0) return null;
|
||||
|
||||
@@ -153,6 +153,7 @@ async function loadActiveAgents() {
|
||||
alarmKeywords: parseList(row.alarm_keywords),
|
||||
sources: parseList(row.sources),
|
||||
excludes: parseList(row.excludes),
|
||||
excludeCompanies: parseList(row.exclude_companies),
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@@ -477,6 +477,8 @@ CREATE TABLE IF NOT EXISTS icon_scanner_agents (
|
||||
-- Schlagwoerter mit Alarm: werden wie `keywords` gesucht und gezaehlt,
|
||||
-- ein Fund meldet sich zusaetzlich mit Ton und Pop-up.
|
||||
alarm_keywords TEXT NULL,
|
||||
-- Unternehmen, deren Anzeigen dieser Agent nie zeigt (z. B. ["Hays"]).
|
||||
exclude_companies TEXT NULL,
|
||||
is_active TINYINT(1) NOT NULL DEFAULT 1,
|
||||
created_by INT UNSIGNED NULL,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
Reference in New Issue
Block a user