DeepSeek V4.1 Flash: native Bilderkennung, GLM/OpenRouter entfernt
- deepseek-flash als einziges Modell mit Vision, Preise aktualisiert - OpenRouter/GLM samt Code, Config und Env-Variablen entfernt - Legacy-Modell-IDs in Sessions werden automatisch migriert - browse lädt Produktbilder lokal nach dashboards/ (Dedupe + Cleanup) - Vision-Prompt: Workflow für visuelle Produktsuche, max. ~5 Screenshots
This commit is contained in:
1 parent
12f01b86d6
commit
b740ce1316
9 files changed
+130
-82
No files matched your search
+76
-5
@@ -1,5 +1,6 @@
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const crypto = require('crypto');
|
||||
const { chromium } = require('playwright');
|
||||
|
||||
const DASHBOARD_DIR = path.join(__dirname, '..', 'dashboards');
|
||||
@@ -92,8 +93,76 @@ async function search(query, log) {
|
||||
return results;
|
||||
}
|
||||
|
||||
/** Seite besuchen und lesbaren Text extrahieren. */
|
||||
async function browse(url, log) {
|
||||
const IMAGE_TYPES = { 'image/jpeg': 'jpg', 'image/png': 'png', 'image/webp': 'webp', 'image/gif': 'gif' };
|
||||
const MAX_IMAGE_BYTES = 4 * 1024 * 1024;
|
||||
const MAX_TOTAL_IMAGE_BYTES = 12 * 1024 * 1024;
|
||||
const MAX_IMAGES = 8;
|
||||
|
||||
/**
|
||||
* Extrahiert Produktbild-URLs (nur absolute http(s), ausreichend groß, dedupliziert).
|
||||
* Läuft im Browser-Kontext der besuchten Seite.
|
||||
*/
|
||||
function collectImages() {
|
||||
const images = [];
|
||||
const seen = new Set();
|
||||
for (const img of document.querySelectorAll('img')) {
|
||||
const src = (img.currentSrc || img.src || '').split('#')[0];
|
||||
if (!/^https?:/i.test(src) || seen.has(src)) continue;
|
||||
const nw = img.naturalWidth || 0;
|
||||
const nh = img.naturalHeight || 0;
|
||||
const rect = img.getBoundingClientRect();
|
||||
const w = nw || rect.width || img.width || 0;
|
||||
const h = nh || rect.height || img.height || 0;
|
||||
if (w < 120 || h < 120) continue;
|
||||
seen.add(src);
|
||||
images.push({ url: src, alt: (img.alt || '').trim().slice(0, 120) });
|
||||
if (images.length >= 12) break;
|
||||
}
|
||||
return images;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lädt extrahierte Bilder lokal in den Dashboards-Ordner, damit das Dashboard
|
||||
* sie unabhängig von Hotlink-Schutz und ablaufenden CDN-URLs einbetten kann.
|
||||
* Dateiname: <sessionId>-img-<hash>.<ext> – wird beim Session-Löschen mit entfernt.
|
||||
*/
|
||||
async function cacheImages(page, images, sessionId, log) {
|
||||
if (!sessionId) return images;
|
||||
fs.mkdirSync(DASHBOARD_DIR, { recursive: true });
|
||||
const existingFiles = new Set(fs.readdirSync(DASHBOARD_DIR));
|
||||
const out = [];
|
||||
let total = 0;
|
||||
for (const img of images) {
|
||||
if (out.length >= MAX_IMAGES || total >= MAX_TOTAL_IMAGE_BYTES) break;
|
||||
try {
|
||||
const hash = crypto.createHash('sha1').update(img.url).digest('hex').slice(0, 12);
|
||||
const known = [...existingFiles].find((f) => f.startsWith(`${sessionId}-img-${hash}.`));
|
||||
if (known) {
|
||||
out.push({ url: `/dashboards/${known}`, alt: img.alt });
|
||||
continue;
|
||||
}
|
||||
const resp = await page.request.get(img.url, {
|
||||
timeout: 15000,
|
||||
headers: { Referer: page.url() },
|
||||
});
|
||||
const type = String(resp.headers()['content-type'] || '').split(';')[0].trim().toLowerCase();
|
||||
const ext = IMAGE_TYPES[type];
|
||||
if (!ext || !resp.ok()) continue;
|
||||
const buf = await resp.body();
|
||||
if (buf.length < 512 || buf.length > MAX_IMAGE_BYTES || total + buf.length > MAX_TOTAL_IMAGE_BYTES) continue;
|
||||
const file = `${sessionId}-img-${hash}.${ext}`;
|
||||
fs.writeFileSync(path.join(DASHBOARD_DIR, file), buf);
|
||||
existingFiles.add(file);
|
||||
total += buf.length;
|
||||
out.push({ url: `/dashboards/${file}`, alt: img.alt });
|
||||
} catch {}
|
||||
}
|
||||
log?.(`Produktbilder lokal gespeichert: ${out.length}`);
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Seite besuchen und lesbaren Text + Produktbilder (lokal gespeichert) extrahieren. */
|
||||
async function browse(url, log, opts = {}) {
|
||||
return withPage(url, async (page) => {
|
||||
const nav = await goto(page, url);
|
||||
if (!nav.ok) return { nav, error: 'Seite konnte nicht geladen werden' };
|
||||
@@ -101,10 +170,12 @@ async function browse(url, log) {
|
||||
const data = await page.evaluate(() => {
|
||||
const title = document.title || '';
|
||||
const text = (document.body ? document.body.innerText : '').replace(/\s+\n/g, '\n');
|
||||
return { title, text: text.slice(0, 15000) };
|
||||
return { title, text: text.slice(0, 6000) };
|
||||
});
|
||||
log?.(`Besuche ${url} -> ${data.title}`);
|
||||
return { nav, title: data.title, text: data.text };
|
||||
const extracted = await page.evaluate(collectImages);
|
||||
const images = await cacheImages(page, extracted, opts.sessionId, log);
|
||||
log?.(`Besuche ${url} -> ${data.title} (${images.length} Bilder lokal)`);
|
||||
return { nav, title: data.title, images, text: data.text };
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user