Scanner: Stellenanzeigen aus Jobbörsen (DACH, max. 21 Tage)
Neuer Menüpunkt Scanner mit Dashboard und Suchagenten, Zeitplan und API-Schlüssel unter Einstellungen -> Scanner. - Quellen: Agentur für Arbeit (API), StepStone, karriere.at, freelancermap (HTML) sowie Adzuna und Jooble (kostenlose Schlüssel) - Quote = gefundene Schlagwörter, Titel zählt doppelt; keine KI - Abgleich mit icon_companies, bekannte Firmen oben mit Abzeichen - Zeitplan Vorgabe stündlich Mo-Fr 06-20 Uhr, einstellbar - Sperre gegen parallele Läufe wie im perinco-Scanner Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
6fe5b18c11
commit
1b82f580db
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* Bewertung einer Anzeige gegen einen Suchagenten - ohne KI.
|
||||
*
|
||||
* Wunsch vom 29.09.2026: kein Abgleich mit Lebenslaeufen, sondern "je mehr
|
||||
* Schlagwoerter gefunden werden, desto besser der Treffer". Gezaehlt wird in
|
||||
* Titel und Text; ein Schlagwort im Titel zaehlt doppelt, weil der Titel die
|
||||
* Rolle benennt und der Text oft nur Umfeld beschreibt.
|
||||
*
|
||||
* score = Summe(Gewicht je gefundenem Schlagwort) / (2 * Anzahl Schlagwoerter)
|
||||
* Gewicht 2 = im Titel, 1 = nur im Text
|
||||
*/
|
||||
|
||||
const normalize = (text) => String(text ?? '').normalize('NFC').toLowerCase();
|
||||
|
||||
const escape = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
|
||||
/**
|
||||
* Kurze Begriffe ("IT", "SAP", "HR") nur als ganzes Wort - sonst steckt "it" in
|
||||
* jedem "Mitarbeiter". Laengere als Teilwort, damit "Projektleiter" auch in
|
||||
* "IT-Projektleiterin" und "Projektleiterstelle" trifft.
|
||||
*/
|
||||
export function containsTerm(text, term) {
|
||||
const needle = normalize(term).trim();
|
||||
if (!needle) return false;
|
||||
const haystack = normalize(text);
|
||||
if (needle.length <= 3) {
|
||||
return new RegExp(`(?<![\\p{L}\\p{N}])${escape(needle)}(?![\\p{L}\\p{N}])`, 'u').test(haystack);
|
||||
}
|
||||
return haystack.includes(needle);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {{ score: number, hits: string[] } | null} null = ausgeschlossen
|
||||
* oder kein einziges Schlagwort gefunden
|
||||
*/
|
||||
export function scoreJob(agent, job) {
|
||||
const title = job.title ?? '';
|
||||
const all = `${title}\n${job.company ?? ''}\n${job.description ?? ''}`;
|
||||
|
||||
if ((agent.excludes ?? []).some((term) => containsTerm(all, term))) return null;
|
||||
|
||||
const keywords = agent.keywords ?? [];
|
||||
if (keywords.length === 0) return null;
|
||||
|
||||
let points = 0;
|
||||
const hits = [];
|
||||
for (const keyword of keywords) {
|
||||
if (containsTerm(title, keyword)) {
|
||||
points += 2;
|
||||
hits.push(keyword);
|
||||
} else if (containsTerm(all, keyword)) {
|
||||
points += 1;
|
||||
hits.push(keyword);
|
||||
}
|
||||
}
|
||||
if (hits.length === 0) return null;
|
||||
|
||||
return { score: Math.round((100 * points) / (2 * keywords.length)), hits };
|
||||
}
|
||||
|
||||
/*
|
||||
* Firmennamen der Boersen und des CRM auf einen Nenner bringen:
|
||||
* "Müller Holding GmbH & Co. KG" -> "müller holding".
|
||||
*/
|
||||
const LEGAL_FORMS =
|
||||
/\b(gmbh|mbh|ag|se|kg|kgaa|ohg|gbr|ug|e\.?\s?v|e\.?\s?k|co|inc|ltd|llc|plc|sa|sarl|bv|nv|haftungsbeschränkt|gesellschaft mit beschränkter haftung|aktiengesellschaft)\b\.?/g;
|
||||
|
||||
export function normalizeCompany(name) {
|
||||
return normalize(name)
|
||||
.replace(/\(.*?\)/g, ' ')
|
||||
.replace(/&/g, ' ')
|
||||
.replace(LEGAL_FORMS, ' ')
|
||||
.replace(/[^\p{L}\p{N}]+/gu, ' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sucht das Unternehmen im CRM. Gleicher Name gewinnt; sonst darf der Name der
|
||||
* Anzeige mit dem CRM-Namen beginnen ("Siemens" -> "Siemens Mobility"), aber
|
||||
* nur ab fuenf Zeichen, sonst wird aus "ABC" jeder, der mit "ABC" anfaengt.
|
||||
*
|
||||
* @param {Array<{id:number, key:string}>} companies vorberechnet, laengste zuerst
|
||||
*/
|
||||
export function findCompany(companies, name) {
|
||||
const key = normalizeCompany(name);
|
||||
if (!key) return null;
|
||||
const exact = companies.find((company) => company.key === key);
|
||||
if (exact) return exact.id;
|
||||
const prefix = companies.find((company) => company.key.length >= 5 && key.startsWith(`${company.key} `));
|
||||
return prefix ? prefix.id : null;
|
||||
}
|
||||
Reference in New Issue
Block a user