
Egy megbeszélés PDF-be nyomtatása PDF-könyvtár nélkül
A GeekBye egy megbeszélést PDF-ként exportál, és sehol a kódban nincs PDF-könyvtár. HTML-t renderel egy láthatatlan böngészőablakban, és azt nyomtatja ki. Ez a döntés az egész történet: könnyűvé tette a funkció megépítését, és megadott neki minden hibát, ami egy valódi böngészőnek van — egy fehér felvillanás, egy URL-hosszkorlát és egy oldaltörés, amely félbevágta a képernyőképeket. A legrondább bug javítása egyetlen sor CSS volt.
Van egy kielégítő trükk elrejtve a GeekBye „exportáld ezt a megbeszélést PDF-ként" gombjában: nincs mögötte PDF-könyvtár. Nincs jsPDF, nincs pdfmake, nincs pdfkit, nincs headless puppeteer. Az egész funkció egy rejtett böngésző. Az alkalmazás épít egy HTML oldalt, amely leírja a megbeszélést — átirat-bejegyzések, képernyőképek, időbélyegek —, betölti egy láthatatlan Electron ablakba, és megkéri azt az ablakot, hogy nyomtassa ki magát PDF-be. A webContents.printToPDF elvégzi a többit. Ez az egyetlen architekturális döntés az egész történet e kiadásban, mert olcsóvá tette a funkció megépítését, és átadott neki minden bugot, ami egy valódi böngészőnek van.
Az „előzmény" ugyanaz a kiadás
Ezek a bejegyzések általában azzal nyitnak, hogy „így működött korábban". Ezúttal nincs korábban. A v1.8.9 changelogja karbantartásként olvasható — "fixed page breaks splitting screenshots and filtered empty entries" — mintha a PDF-export egy meglévő funkció lenne, amely javítást kap. Nem volt az. Az első commit, amely bevezeti, a feat: add PDF export via Electron printToPDF, három nappal ugyanabba a kiadási ciklusba érkezik, és minden „fix" a changelogban ugyanazon a héten történő iteráció órákkal korábbi kódon. A funkció megszületett, eltört és megszilárdult, mind egyetlen tagen belül. Érdemes ezt hangosan kimondani, mert ez a szállítás sok esetének őszinte alakja: a kiadási jegyzetek csiszolt pontja és a dolog rendetlen születése gyakran ugyanaz a kiadás.
Miért egyáltalán rejtett böngésző
A mechanizmust érdemes megérteni a bugok előtt, mert az összes bug belőle származik. Minden egyetlen fő-folyamat fájlban él, a electron/ipcHandlers/pdfExportHandler.ts-ben. Amikor a renderer elsüti a 'pdf:export-report' IPC-t, a handler:
- Meghívja a
buildReportHtml()-t, hogy összeállítson egy teljes HTML stringet a megbeszélés timeline-jából — egy.itemblokk átirat-bejegyzésenként vagy képernyőképenként. - Kiírja azt a HTML-t egy ideiglenes fájlba az
os.tmpdir()-ben. - Betölti a fájlt egy rejtett ablakba —
new BrowserWindow({ show: false, opacity: 0, focusable: false, skipTaskbar: true }). - Megvárja a képek betöltődését, majd meghívja a
win.webContents.printToPDF({ printBackground: true, margins: { … } })-t. - Elmenti az eredményt az
app.getPath('downloads')-baMeeting-Report-<timestamp>.pdfnéven.
A vonzerő nyilvánvaló. Egy PDF-könyvtár arra kényszerít, hogy mindent kézzel pozicionálj — rajzolj szöveget az (x, y)-nál, mérd meg, léptesd a kurzort, kezeld magad az oldaltöréseket. Egy böngésző már mindezt megteszi: <div>-eket és CSS-t írsz, és a Chromium elrendezésmotorja lapoz helyetted. Nem egy drawing API-t tanulsz; egy weboldalt írsz. Egy olyan dokumentumhoz, amely többnyire stílusozott szöveg és beágyazott képek, ez hatalmas rövidítés.
A bökkenő az, hogy egy böngészőt is örököltél, annak minden peremesetével. Három közülük az első négy napon belül felbukkant.
A fehér felvillanás. Egy rejtett ablak macOS-en nem megbízhatóan rejtett. Az első próbálkozások a képernyőn kívülre pozicionálták (x: screenWidth + 1000), és még mindig felvillantott egy fehér téglalapot a képernyőn egy képkockára a nyomtatás előtt. A javítás, három próbálkozás után, az volt, hogy ne támaszkodjon a pozícióra vagy a show: false-ra, és állítson be opacity: 0-t — egy teljesen átlátszó ablak nem villan fel, még akkor sem, ha a compositor rövid időre megmutatja. A commit nyíltan kimondja: "show:false with off-screen positioning still flashes on macOS."
Az URL-hosszkorlát. Az első verzió az egész HTML-t — képernyőképekkel együtt — data: URL-ként ágyazta be. Ez addig működik, amíg egy megbeszélésben nincs néhány base64-kódolt képernyőkép, amely ponton az URL túlszáll a Chromium ~2 MB-os data-URL-korlátján, és a betöltés egyenesen ERR_INVALID_URL-lel bukik. Ezért a fenti 2. lépés egy ideiglenes fájlt ír, és loadFile()-t csinál, ahelyett, hogy egy data URL-t töltene be: egy fájlútvonalnak nincs hosszkorlátja. (Az a ~2 MB az egyetlen szám ebben az egész történetben, és egy soron belüli becslés egy kódkommentben, nem valami, amit bárki benchmarkolt volna — kezeld úgy, mint „elég nagy ahhoz, hogy beleüss", nem mint pontos küszöböt.)
A nem-escapelt szöveg. Az átirat-szöveg tetszőleges felhasználói tartalom, és egyenesen a HTML stringbe került. Bármely átirat, amely történetesen valami olyat tartalmazott, mint <div> vagy <script>, eltörte volna az oldalt, vagy rosszabb. Így a handler kinövesztett egy escapeHtml() menetet minden stringen, amit interpolál. Abban a pillanatban, amikor a PDF-ed egy weboldal, a HTML-injektálás is a te problémád.
Az üres dobozok, amelyek nem voltak üresek
Most a címlaphiba, mert ez a legjobb. A felhasználók üres dobozokkal rendelkező exportált PDF-eket jelentettek — bejegyzések, amelyek időbélyeget és badge-et mutattak, de semmilyen képernyőképet, csak üres helyet ott, ahol a képnek lennie kellene. A nyilvánvaló olvasat az, hogy „az export eldobja a képadatokat", és a nyilvánvaló javítás az, hogy „szűrd ki az üres bejegyzéseket". A csapat valóban hozzáadta azt a szűrőt — erről egy másodperc múlva többet — de nem az volt a valódi ok.
A valódi ok a lapozás volt. Minden bejegyzés egy .item konténer, amely egy időbélyeget, egy badge-et és egy képet tart. A Chromium, PDF-be nyomtatva, boldogan tört meg egy oldalt egy item közepén — az időbélyeget és a badge-et az egyik oldal aljára, a képet pedig a következőre renderelve. Ami üres doboznak tűnt, az egy item felső fele volt, amelynek a képe a következő oldalra túlfolyt. Az adat mind ott volt; az elrendezés lenyakazta. A commit őszinte a téves diagnózisról: "The empty boxes in exported PDFs were caused by Chromium's printToPDF splitting .item containers across pages — the time/badge rendered on one page while the image overflowed to the next."
A javítás egyetlen sor CSS:
.item {
break-inside: avoid;
}
A break-inside: avoid azt mondja az elrendezésmotornak, hogy tartson egy itemet egyben — ha nem fér el egy oldal fennmaradó helyén, mozgassa az egész itemet a következő oldalra, ahelyett, hogy feldarabolná. Ez a rejtett-böngésző architektúra hozadéka kicsiben megfogalmazva: a funkció legrondább, legtöbbet jelentett bugja nem oldalmagasság-aritmetikával és manuális addPage() hívásokkal lett javítva, hanem egyetlen CSS-tulajdonsággal, amelyet a böngésző már tudott, hogyan tiszteljen. Nem írtál lapozót; megkérted azt, amelyet örököltél, hogy viselkedjen.
A ténylegesen üresek szűrésének két rétege
Voltak valóban üres bejegyzések is — csak nem azok, amelyek a dobozokat okozták. Üres duplikátumok egy gyors dupla Cmd+Enterből, előnézet nélküli képernyőképek, átirat-bejegyzések, amelyek csak whitespace-ből álltak. Ezek kiszűrődtek, és érdekes módon kétszer szűrődtek ki, két rétegben:
- A handlerben a
buildReportHtml()őrzi minden itemet —if (!item.content?.trim()) return ''szövegre, és képekreconst safeSrc = item.preview?.startsWith('data:image/') ? item.preview : ''; if (!safeSrc) return ''— majd a.filter(Boolean)eldobja az üreseket. Az az utolsó ellenőrzés dupla munkát végez: egy érvényességi kapu és egy kis biztonsági ellenőrzés, hogy csak valódi képadat éri el valaha az oldalt. - A timeline forrásánál, a rendererben, ugyanaz az üresség szűrődik ki, mielőtt egy itemet valaha hozzáadnának —
if (!entry.text?.trim()) return,if (!screenshot.preview) return.
A történet szép részlete a módszer. Az első commit hozzáadott egy logger.debug('PDF timeline item', …) ciklust kifejezetten „az üres bejegyzések debugolásához". Amint az a naplózás felfedte, honnan jöttek valójában az üresek — beleértve egy külön képernyőkép-deduplikációs bugot, ahol a gyors felvételek üres duplikátumokat termeltek — a következő commit kijavította a gyökérokot feljebb, és eltávolította a debug-naplózást. Adj hozzá műszerezést, hogy megtaláld az okot, javítsd az okot, töröld a műszerezést. A changelog "filtered empty entries" pontja csendben tartalmazza ezt az egész add-aztán-távolítsd ívet.
v1.8.12: "excludes AI responses," olvasd a diffet
Három kiadással később, még egy változás — és jó lecke a kód olvasásában a changelog helyett. A v1.8.12 jegyzete azt mondja, hogy a PDF-export mostantól "excludes AI responses for cleaner meeting records." Igaz, de hiányos.
Az export-timeline minden itemet megjelöl egy type-pal: 'transcript' | 'chat_user' | 'chat_assistant' | 'screenshot'. Egy kimondott megbeszélés-sor egy transcript; egy kérdés, amelyet a segéd paneljébe gépeltél, egy chat_user; az AI válasza egy chat_assistant. A v1.8.12 változtatása egy kilencsoros szerkesztés egy renderer fájlban, és a működő sor egy szűrő, amelyet közvetlenül az export payload felépítése elé helyeztek:
timeline.filter((item) => item.type === 'transcript' || item.type === 'screenshot')
Ez egy allowlist, nem blocklist. Nem távolítja el a chat_assistant itemeket; csak transcript-et és screenshot-ot tart meg, és mindent mást eldob — ami azt jelenti, hogy lecsupaszítja a felhasználó saját chat_user kérdéseit az AI válaszaival együtt. A commit törzse pontosabb, mint a changelog: "PDF export keeps only transcripts and screenshots, no chat messages." Az exportált feljegyzés mostantól tisztán a megbeszélés — ami elhangzott és ami a képernyőn volt —, a segéd oldalsó paneljének teljes beszélgetése eltávolítva belőle.
És ez a helyes döntés, termékszempontból. A GeekBye chatje egy privát segéd, amelyet egy hívás közben konzultálsz — kérdezd meg egy képernyőképről, kapj egy gyors magyarázatot. Azok a párbeszédek a jegyzetfüzeted, nem a megbeszélés része. Egy megbeszélés-feljegyzésnek, amelyet megosztanál valakivel, a megbeszélést kellene tartalmaznia, nem a mellékbeszélgetéseidet egy AI-jal róla. Az allowlist egyszerűen „a megbeszéléssé" teszi annak szó szerinti definícióját, ami exportálódik.
Három dolog, amit ez a kiadás tanított nekünk
- Egy böngésző egy PDF-renderer, amely már megvan neked. Ha a dokumentumod strukturált szöveg és képek, a
printToPDFegy rejtettBrowserWindowfelett valódi lapozást, valódi betűrenderelést és CSS-elrendezést ad neked ingyen — semmilyen PDF drawing API. Egy könyvtárfüggőséget cserélsz böngésző-peremesetekre, ami gyakran a jobb csere. - A bug, amit látsz, és a bug, amely létezik, nem mindig ugyanaz. Az „üres dobozok" hiányzó adatot üvöltöttek; az ok egy feldarabolt konténer volt. Műszerezz, mielőtt feltételezel — a csapat debug-naplózása az, ami egy valószínű-de-rossz javítást (szűrd az üreseket) a valódivá (
break-inside: avoid) alakított. - A changelog egy összefoglaló; a szűrő az igazság. Az "Excludes AI responses" egy allowlist, amely a saját chat-kérdéseidet is eldobja. Amikor egy kiadási jegyzet egy viselkedésváltozást ír le, a diff megmondja a pontos éleit — és itt a pontos él az, hogy „csak a megbeszélés marad meg".
A v1-történet előző fejezetéért lásd az OCR-től a pixelekig, a fallback elvesztése nélkül (v1.8.6–v1.8.7); a teljes ívért pedig a szoftver tökéletesre szállításának anatómiája.