Steven
Steven5 นาทีที่อ่าน

พิมพ์การประชุมเป็น PDF โดยไม่มี PDF library

GeekBye export การประชุมเป็น PDF ทั้งที่ในโค้ดไม่มี PDF library อยู่ที่ไหนเลย มัน render HTML ในหน้าต่าง browser ที่มองไม่เห็นแล้วพิมพ์มันออกมา ทางเลือกนั้นแหละคือทั้งเรื่อง: มันทำให้ฟีเจอร์นี้สร้างง่าย และมอบทุกความล้มเหลวที่ browser จริง ๆ มีให้มัน — แฟลชขาว, ลิมิตความยาว URL, และ page break ที่ตัด screenshot ขาดครึ่ง ทางแก้ของ bug ที่น่าเกลียดที่สุดคือ CSS หนึ่งบรรทัด

วิศวกรรม
Electron
Desktop
GeekBye รีลีส
พิมพ์การประชุมเป็น PDF โดยไม่มี PDF library

ในปุ่ม «export การประชุมนี้เป็น PDF» ของ GeekBye มีทริกที่น่าพอใจซ่อนอยู่: เบื้องหลังมันไม่มี PDF library เลย ไม่มี jsPDF, ไม่มี pdfmake, ไม่มี pdfkit, ไม่มี puppeteer แบบ headless ทั้งฟีเจอร์คือ browser ที่ซ่อนไว้ตัวหนึ่ง แอปสร้างหน้า HTML ที่พรรณนาการประชุม — entry ของ transcript, screenshot, timestamp — โหลดมันเข้าหน้าต่าง Electron ที่มองไม่เห็น แล้วขอให้หน้าต่างนั้นพิมพ์ตัวเองเป็น PDF ที่เหลือ webContents.printToPDF จัดการเอง ทางเลือกทางสถาปัตยกรรมเดียวนั้นแหละคือทั้งเรื่องของรีลีสนี้ เพราะมันทำให้ฟีเจอร์สร้างได้ถูก และยื่นทุก bug ที่ browser จริงมีให้มัน

«ก่อนหน้า» ก็คือรีลีสเดียวกัน

ปกติโพสต์แบบนี้เปิดด้วย «นี่คือวิธีที่มันทำงานเมื่อก่อน» คราวนี้ไม่มีเมื่อก่อน changelog ของ v1.8.9 อ่านเหมือนงาน maintenance — "fixed page breaks splitting screenshots and filtered empty entries" — ราวกับว่า PDF export เป็นฟีเจอร์เดิมที่กำลังโดน patch มันไม่ใช่ commit แรกที่แนะนำมันเข้ามา, feat: add PDF export via Electron printToPDF, ลงจอดในวันที่สามของ release cycle เดียวกัน และทุก «fix» ใน changelog คือการวนแก้ในสัปดาห์เดียวกันบนโค้ดที่เพิ่งเกิดมาไม่กี่ชั่วโมง ฟีเจอร์นี้ถือกำเนิด, พัง, และถูกทำให้แข็งแรง ทั้งหมดภายในแท็กเดียว ที่พูดออกมาดัง ๆ ก็เพราะนั่นคือรูปร่างที่ซื่อสัตย์ของการ ship หลาย ๆ ครั้ง: bullet ที่ขัดเงาใน release notes กับการถือกำเนิดอันรุงรังของสิ่งนั้น บ่อยครั้งเป็นรีลีสเดียวกัน

ทำไมต้อง browser ที่ซ่อนไว้ตั้งแต่แรก

กลไกนี้ควรเข้าใจก่อนถึง bug เพราะ bug ทั้งหมดสืบทอดมาจากมัน ทุกอย่างอยู่ในไฟล์ main-process ไฟล์เดียว, electron/ipcHandlers/pdfExportHandler.ts เมื่อ renderer ยิง IPC 'pdf:export-report', handler จะ:

  1. เรียก buildReportHtml() เพื่อประกอบ HTML string เต็ม ๆ จาก timeline ของการประชุม — หนึ่ง .item block ต่อหนึ่ง entry ของ transcript หรือ screenshot
  2. เขียน HTML นั้นลงไฟล์ชั่วคราวใน os.tmpdir()
  3. โหลดไฟล์นั้นเข้าหน้าต่างที่ซ่อนไว้ — new BrowserWindow({ show: false, opacity: 0, focusable: false, skipTaskbar: true })
  4. รอให้ภาพโหลดเสร็จ แล้วเรียก win.webContents.printToPDF({ printBackground: true, margins: { … } })
  5. บันทึกผลลัพธ์ไปที่ app.getPath('downloads') ในชื่อ Meeting-Report-<timestamp>.pdf

เสน่ห์มันชัดเจน PDF library บังคับให้คุณวางตำแหน่งทุกอย่างด้วยมือ — วาด text ที่ (x, y), วัดมัน, เลื่อน cursor, จัดการ page break เอง browser ทำทั้งหมดนั้นให้อยู่แล้ว: คุณเขียน <div> กับ CSS แล้ว layout engine ของ Chromium ก็แบ่งหน้าให้คุณ คุณไม่ได้กำลังเรียน drawing API; คุณกำลังเขียนหน้าเว็บ สำหรับเอกสารที่ส่วนใหญ่เป็น text ที่จัดสไตล์และภาพที่ฝังไว้ นั่นคือทางลัดมหึมา

จุดที่ต้องระวังคือคุณก็รับมรดก browser มาด้วย พร้อมทุก edge case ของมัน สามในนั้นโผล่มาภายในสี่วันแรก

แฟลชขาว หน้าต่างที่ซ่อนไว้บน macOS ไม่ได้ซ่อนอย่างน่าเชื่อถือ ความพยายามแรกวางมันไว้นอกจอ (x: screenWidth + 1000) แล้วมันก็ยังแฟลชสี่เหลี่ยมขาวขึ้นจอหนึ่งเฟรมก่อนพิมพ์ ทางแก้ หลังจากสามครั้ง คือเลิกพึ่งตำแหน่งหรือ show: false แล้วตั้ง opacity: 0 — หน้าต่างที่โปร่งใสสนิทไม่แฟลช แม้ compositor จะแสดงมันแวบหนึ่งก็ตาม commit พูดตรง ๆ ว่า: "show:false with off-screen positioning still flashes on macOS."

ลิมิตความยาว URL เวอร์ชันแรก inline HTML ทั้งก้อน — screenshot และทุกอย่าง — เป็น data: URL นั่นใช้ได้จนกว่าการประชุมจะมี screenshot ที่ encode เป็น base64 อยู่ไม่กี่รูป ณ จุดนั้น URL ก็แล่นทะลุลิมิต data-URL ~2 MB ของ Chromium แล้วการโหลดล้มเหลวไปเลยด้วย ERR_INVALID_URL นั่นคือเหตุผลที่ขั้นตอนที่ 2 ข้างบนเขียน ไฟล์ ชั่วคราวแล้วทำ loadFile() แทนการโหลด data URL: path ของไฟล์ไม่มีลิมิตความยาว (~2 MB นั่นเป็นตัวเลขเดียวในทั้งเรื่องนี้ และมันเป็นการประมาณแบบ inline ในคอมเมนต์โค้ด ไม่ใช่สิ่งที่ใครไป benchmark มา — ถือว่ามัน «ใหญ่พอที่จะชน» ไม่ใช่ threshold ที่แม่นยำ)

text ที่ไม่ได้ escape transcript text เป็น user content ตามอำเภอใจ และมันถูกหย่อนลงไปใน HTML string ตรง ๆ transcript อันไหนที่บังเอิญมีอะไรอย่าง <div> หรือ <script> ก็จะทำหน้าพัง หรือแย่กว่านั้น ดังนั้น handler จึงงอกการทำ escapeHtml() หนึ่งรอบครอบทุก string ที่มัน interpolate นาทีที่ PDF ของคุณเป็นหน้าเว็บ HTML injection ก็เป็นปัญหาของคุณด้วยเช่นกัน

กล่องว่างที่ไม่ได้ว่าง

ทีนี้ถึง bug พาดหัว เพราะมันเป็นอันที่ดีที่สุด ผู้ใช้รายงาน PDF ที่ export ออกมามี กล่องว่าง — entry ที่แสดง timestamp กับ badge แต่ไม่มี screenshot มีแค่พื้นที่ว่างตรงที่ภาพควรอยู่ การอ่านที่เห็นชัดคือ «การ export กำลังทิ้งข้อมูลภาพ» และการแก้ที่เห็นชัดคือ «filter entry ว่างออก» ทีมได้เพิ่ม filter นั้นจริง ๆ — เดี๋ยวจะพูดถึงมันอีกที — แต่มันไม่ใช่สาเหตุจริง

สาเหตุจริงคือการแบ่งหน้า แต่ละ entry คือ .item container ที่อุ้ม timestamp, badge, และภาพหนึ่งภาพ Chromium ตอนพิมพ์เป็น PDF ยินดีที่จะตัดหน้า กลาง item — render timestamp กับ badge ไว้ล่างสุดของหน้าหนึ่ง แล้วดันภาพไปหน้าถัดไป สิ่งที่ดูเหมือนกล่องว่างคือครึ่งบนของ item ที่ภาพของมันล้นไปหน้าถัดมา ข้อมูลอยู่ครบ; layout ต่างหากที่เอามันขึ้นกิโยติน commit พูดตรงไปตรงมาเรื่องการวินิจฉัยผิดนี้: "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."

ทางแก้คือ CSS หนึ่งบรรทัด:

.item {
  break-inside: avoid;
}

break-inside: avoid บอก layout engine ให้เก็บ item ไว้ทั้งอัน — ถ้ามันไม่พอดีกับพื้นที่ที่เหลือบนหน้า ให้ย้ายทั้ง item ไปหน้าถัดไปแทนที่จะตัดมัน นี่คือผลตอบแทนของสถาปัตยกรรม browser-ซ่อนที่กล่าวไว้ในภาพย่อ: bug ที่น่าเกลียดที่สุดและถูกรายงานมากที่สุดในฟีเจอร์นี้ ถูกแก้ไม่ใช่ด้วยเลขคณิตความสูงของหน้าและการเรียก addPage() ด้วยมือ แต่ด้วย CSS property เดียวที่ browser รู้วิธีเคารพมันอยู่แล้ว คุณไม่ได้เขียนตัวแบ่งหน้า; คุณขอตัวที่คุณรับมรดกมาให้ทำตัวดี ๆ

สองชั้นของการ filter ของที่ว่างจริง ๆ

มัน มี entry ที่ว่างจริง ๆ อยู่ด้วย — แค่ไม่ใช่พวกที่ทำให้เกิดกล่อง สำเนาว่างจากการกด Cmd+Enter รัว ๆ สองที, screenshot ที่ไม่มี preview, entry ของ transcript ที่เป็นแค่ whitespace พวกนี้ถูก filter และที่น่าสนใจคือมันถูก filter สองครั้ง ในสองชั้น:

  • ใน handler, buildReportHtml() การ์ดแต่ละ item — if (!item.content?.trim()) return '' สำหรับ text และสำหรับภาพ const safeSrc = item.preview?.startsWith('data:image/') ? item.preview : ''; if (!safeSrc) return '' — แล้ว .filter(Boolean) ก็ทิ้งของว่าง เช็คตัวสุดท้ายนั้นทำหน้าที่สองอย่าง: มันเป็นด่านความถูกต้อง และ เป็นเช็คความปลอดภัยเล็ก ๆ ว่ามีแต่ข้อมูลภาพจริง ๆ เท่านั้นที่ไปถึงหน้า
  • ที่ต้นทาง timeline, ใน renderer, ความว่างเดียวกันถูก filter ก่อนที่ item จะถูกเพิ่มเข้าไปเสียอีก — if (!entry.text?.trim()) return, if (!screenshot.preview) return

รายละเอียดที่น่ารักในประวัติคือวิธีการ commit แรกเพิ่ม loop logger.debug('PDF timeline item', …) อย่างชัดเจนว่า "for debugging empty entries" พอ logging นั้นเผยว่าที่ว่างมาจากไหนกันแน่ — รวมถึง bug การ dedupe screenshot อีกตัวหนึ่งที่การ capture รัว ๆ ผลิตสำเนาว่าง — commit ที่ตามมาก็แก้ root cause ที่ต้นน้ำ และ ลบ debug logging ออก เพิ่มเครื่องมือวัดเพื่อหาสาเหตุ, แก้สาเหตุ, ลบเครื่องมือวัด bullet ใน changelog "filtered empty entries" บรรจุส่วนโค้งเพิ่ม-แล้ว-ลบทั้งหมดนั้นไว้อย่างเงียบ ๆ

v1.8.12: "excludes AI responses" อ่าน diff เถอะ

สามรีลีสถัดมา มีการเปลี่ยนอีกอย่าง — และมันเป็นบทเรียนที่ดีเรื่องการอ่านโค้ดแทน changelog โน้ต v1.8.12 บอกว่า PDF export ตอนนี้ "excludes AI responses for cleaner meeting records" จริง แต่ไม่ครบ

timeline ของการ export แท็กแต่ละ item ด้วย type: 'transcript' | 'chat_user' | 'chat_assistant' | 'screenshot' บรรทัดการประชุมที่พูดออกมาคือ transcript; คำถามที่คุณพิมพ์เข้าไปใน assistant panel คือ chat_user; คำตอบของ AI คือ chat_assistant การเปลี่ยนใน v1.8.12 เป็นการแก้เก้าบรรทัดในไฟล์ renderer ไฟล์เดียว และบรรทัดที่ทำงานคือ filter ที่วางไว้ก่อน payload ของการ export จะถูกสร้างพอดี:

timeline.filter((item) => item.type === 'transcript' || item.type === 'screenshot')

นั่นคือ allowlist ไม่ใช่ blocklist มันไม่ได้เอา item ที่เป็น chat_assistant ออก; มันเก็บแต่ transcript กับ screenshot และทิ้งอย่างอื่นทั้งหมด — ซึ่งแปลว่ามันลอกคำถาม chat_user ของผู้ใช้เองออกไปพร้อมกับคำตอบของ AI ตัว commit body แม่นกว่า changelog: "PDF export keeps only transcripts and screenshots, no chat messages." record ที่ export ออกมาตอนนี้เป็นการประชุมล้วน ๆ — อะไรที่ถูกพูดและอะไรที่อยู่บนจอ — โดยบทสนทนาใน side-panel ของ assistant ทั้งหมดถูกเอาออกไป

และนั่นคือการตัดสินใจที่ถูก ในเชิงผลิตภัณฑ์ แชทของ GeekBye เป็นผู้ช่วยส่วนตัวที่คุณปรึกษา ระหว่าง สาย — ถามมันเรื่อง screenshot, ได้คำอธิบายไว ๆ การแลกเปลี่ยนพวกนั้นคือกระดาษทดของคุณ ไม่ใช่ส่วนหนึ่งของการประชุม record การประชุมที่คุณจะแชร์ให้ใครควรมีการประชุม ไม่ใช่บทสนทนาข้างเคียงของคุณกับ AI เรื่องมัน allowlist แค่ทำให้ «การประชุม» เป็นนิยามตามตัวอักษรของสิ่งที่ถูก export

สามอย่างที่รีลีสนี้สอนเรา

  1. browser คือ PDF renderer ที่คุณมีอยู่แล้ว ถ้าเอกสารของคุณคือ text ที่มีโครงสร้างและภาพ, printToPDF ผ่าน BrowserWindow ที่ซ่อนไว้ ให้การแบ่งหน้าจริง, การ render font จริง, และ CSS layout ฟรี ๆ — ไม่ต้องมี drawing API ของ PDF คุณแลก dependency ของ library กับ edge case ของ browser ซึ่งบ่อยครั้งเป็นการแลกที่ดีกว่า
  2. bug ที่คุณเห็นกับ bug ที่มีอยู่จริงไม่ได้เป็นอันเดียวกันเสมอ «กล่องว่าง» ตะโกนว่า ข้อมูลหาย; สาเหตุคือ container ที่ถูกตัด วัดก่อนจะเดา — debug logging ของทีมนั่นแหละที่พลิกการแก้ที่ดูสมเหตุสมผลแต่ผิด (filter ของว่าง) ให้เป็นอันจริง (break-inside: avoid)
  3. changelog คือบทสรุป; filter คือความจริง "Excludes AI responses" เป็น allowlist ที่ทิ้งคำถามแชทของคุณเองไปด้วย เมื่อ release note พรรณนาการเปลี่ยนพฤติกรรม diff จะบอกขอบเขตที่แน่นอนของมันให้คุณ — และตรงนี้ขอบที่แน่นอนคือ «มีแต่การประชุมที่รอด»

สำหรับบทก่อนหน้าในเรื่องยุค v1 ดูจาก OCR สู่ pixel โดยไม่ทิ้ง fallback (v1.8.6–v1.8.7); และสำหรับภาพรวมทั้งเส้นเรื่อง ดูกายวิภาคของการส่งมอบซอฟต์แวร์ให้สมบูรณ์แบบ

บทความที่เกี่ยวข้อง

สองโหมดความล้มเหลวของ overlay แบบคลิกทะลุ
Steven
Steven4 นาทีที่อ่าน

สองโหมดความล้มเหลวของ overlay แบบคลิกทะลุ

หน้าต่างของ GeekBye ลอยอยู่เหนือทุกอย่าง และปล่อยให้คลิกของคุณผ่านทะลุมันไป — ยกเว้นตรงที่มันมีปุ่ม นั่นคือสัญญาสองด้าน และ v1.8.5 กับ v1.8.14 คือหน้าตาของตอนที่แต่ละด้านพัง: รีลีสหนึ่ง overlay กลืน dialog ของระบบไป, อีกรีลีสหนึ่งมันขโมยการกดปุ่มของคุณ ทางแก้ที่ชนะสำหรับอันที่สองคือการลบโค้ดทิ้ง

วิศวกรรม
Electron
Desktop
ถือกำเนิดและถูกแก้ในสิบสองนาที
Steven
Steven4 นาทีที่อ่าน

ถือกำเนิดและถูกแก้ในสิบสองนาที

changelog ของ GeekBye v1.8.10 บอกว่ามันแก้ crash ตอนแก้ไข keyboard shortcut มันแก้จริง — แต่ crash นั้นถูกใส่เข้ามาและถูกแก้ใน pull request เดียวกัน ห่างกันสิบสองนาที และไม่เคยไปถึงผู้ใช้แม้แต่คนเดียว เรื่องจริงคือปฏิกิริยาลูกโซ่ด้านความน่าเชื่อถือที่ผลิตมันออกมา: การเปลี่ยนแปลงเล็ก ๆ ที่ถูกต้องเรื่องว่าจะ suspend shortcut เมื่อไหร่ และ React teardown bug ที่หล่นออกมาจากโค้ดบรรทัดเดียวที่ตั้งใจให้ editor ปลอดภัยขึ้น

วิศวกรรม
React
Desktop
จาก OCR สู่ pixel โดยไม่ทิ้ง fallback
Steven
Steven4 นาทีที่อ่าน

จาก OCR สู่ pixel โดยไม่ทิ้ง fallback

GeekBye เลิกอ่าน screenshot ของคุณด้วย OCR แล้วเริ่มส่งภาพจริง ๆ ไปให้ vision model — ซึ่งเป็นส่วนที่ง่าย ส่วนที่ให้บทเรียนคือการ optimize ที่ข้าม OCR ไปทั้งหมดสำหรับผู้ใช้ vision ซึ่งทำให้ fallback ไปยัง model ที่ไม่รองรับ vision พังไปเงียบ ๆ และต้องถอยกลับไปยังเวอร์ชันที่ตาข่ายนิรภัยทำงานขนานกันและแทบไม่มีต้นทุนเลย

วิศวกรรม
AI
Desktop