בניתי לחנות אופניים ישראלית תוסף Chrome שמוסיף יכולות CRM מעל WhatsApp Web: הוא קורא את השיחה הפתוחה, מציג את הקשר הלקוח מ-Shopify, מציע תשובות בעברית, ובסוף מסכם את השיחה ומתעד אותה בכרטיס הלקוח. הוא קורא בלבד — לא כותב לוואטסאפ, לא שולח הודעות, ולא נוגע ב-API. זה מה שאפשר להם לקבל את מה שרצו מ-CRM בלי לוותר על הממשק הרגיל, ובלי לשלם על עוד מערכת.
הבחירה שוואטסאפ מכריחה
הגעתי לחנות, והוזמנתי לקומה העליונה. בין מבוך ארגזים של אופניים יש שולחן עבודה קטן עם מחשב וערימת קבלות. מנהל החנות יושב מול המחשב והטלפון, ועונה בעצמו לכל לקוח שפונה בשתי הפלטפורמות.
שירות טוב ואדיב הוא הערך המרכזי של העסק, וזה ניכר. כל לקוח מקבל את מלוא תשומת הלב של מנהל החנות בכבודו ובעצמו. גם ברור שזה לוקח הרבה יותר מדי קשב ניהולי.
הם כבר ניסו להעביר את שירות הלקוחות ל-CRM שעובד מול WhatsApp API, ונכוו. הם נתקלו בדבר שרוב בעלי העסקים מגלים מאוחר מדי: WhatsApp מאלצים לבחור — או הממשק הקלאסי, או ה-API. אי אפשר גם וגם. ברגע שבוחרים API, העסק ננעל לממשק של ה-CRM, והמנהל כבר לא יכול "לקחת שליטה" מוואטסאפ הרגיל בנייד או במחשב שלו. זה מאוד לא נוח, במיוחד כשהשירות האישי הוא בדיוק מה שמייחד אותך.
תוסף שקורא בלבד עוקף את הבחירה הזאת לגמרי. הוא לא לקוח לא רשמי, הוא לא מתחבר במקומך, והוא לא נוגע ב-API — ולכן הממשק הרגיל ממשיך לעבוד בדיוק כמו קודם, בכל מכשיר.
סרגל צד, לא הזרקה לדף
הפאנל הוא Chrome Side Panel — משטח של הדפדפן לצד הטאב, לא אלמנטים בתוך הדף של WhatsApp. סקריפט התוכן קורא את ה-DOM שכבר הוצג, בערך כמו שקורא מסך עושה, ולא עושה כלום מעבר לכך: לא משנה DOM, לא משגר אירועים סינתטיים, לא נוגע במודולים הפנימיים.
כמה החלטות שנראות קטנות והן מה שמחזיק את זה:
- MutationObserver על שורש האפליקציה, עם debounce של 400ms. ה-
#mainמוחלף בכל מעבר בין צ'אטים, אז אי אפשר להאזין רק לו. ה-debounce הוא מה שמונע מהתצפית הרחבה להיות יקרה. - 40 ההודעות האחרונות בלבד. הקורא חותך את החלון כדי לתחום את גודל ה-payload שיוצא מהמחשב. זה גם גבול פרטיות וגם גבול עלות.
- hash לזיהוי שינוי. אם התמונה לא השתנתה, לא נשלח snapshot חדש. כשהטאב מוסתר, התצפית נעצרת.
- כל הסלקטורים בבלוק אחד בראש הקובץ. WhatsApp משנים את המבנה בלי הודעה מראש, וכשזה קורה הפאנל פשוט מתרוקן. זה לא ממשק יציב ואין טעם להעמיד פנים שכן — לפחות שיהיה מקום אחד לתקן בו.
מה שאין בקוד: אין מסלול שמקליד לתוך WhatsApp ואין מסלול ששולח. לחיצה על הצעה מעתיקה אותה ללוח, והנציג מדביק, עורך אם בא לו, ולוחץ שלח. כל הודעה שיצאה מהחשבון נשלחה בידי אדם.
למה יש פרוקסי מקומי באמצע
אי אפשר לקרוא ל-Bedrock מהתוסף — זה היה שם את מפתחות ה-AWS בתוך הדפדפן, שם כל אחד יכול לחלץ אותם. אז יש שרת Node קטן שרץ מקומית ומחזיק את המפתחות, ומול הדפדפן הוא חושף ארבע נקודות קצה בלבד: חיפוש לקוח, הצעות, סיכום ובדיקת בריאות.
שני דברים מגבילים את החשיפה שלו. הוא מאזין ל-127.0.0.1 ולא ל-0.0.0.0, אחרת כל מי שנמצא על אותה רשת יכול להריץ קריאות ל-Bedrock על החשבון שלך. והוא בודק את כותרת ה-Origin מול רשימת היתר ומחזיר 403 לפני שהוא פונה למודל — כי האזנה ל-loopback לבדה לא מספיקה: כל אתר שתבקר בו יכול לשלוח בקשות ל-127.0.0.1 מהדפדפן שלך. וחשוב מה זה לא: כברירת מחדל רשימת ההיתר מקבלת כל מקור chrome-extension:// (אפשר לקבע מקור מדויק), ועדיין אין שם אימות ואין הגבלת קצב. זה מכיל את החור, לא סוגר אותו.
ברירת המחדל היא Bedrock באזור פרנקפורט, כדי שההודעות של הלקוחות יישארו באיחוד האירופי. אלה הודעות פרטיות של אנשים שלא התקינו כלום ולא ביקשו כלום, והשאלה איפה הן מעובדות צריכה להיות החלטה, לא ברירת מחדל שהתגלגלה.
שלוש תשובות, לא אחת
האייג'נט לא מנסה להיות הנציג. הוא מקבל את מהלך השיחה ואת פרטי הלקוח, ומחזיר שלוש הצעות בעברית בשלוש זוויות שונות: תשובה ישירה ועניינית, תשובה שמקדמת בעדינות שדרוג רלוונטי, ותשובה אמפתית שמטפלת בהתלבטות או בתלונה. הפרוקסי מוסיף בראש הרשימה תשובה דטרמיניסטית שנשענת על רשומת הלקוח עצמה, אז בפועל הנציג רואה שלוש או ארבע. הנציג בוחר.
בסוף שיחה יש כפתור "סכם שיחה" — הסיכום הוא שניים עד ארבעה משפטים בגוף שלישי: מה הלקוח רצה, מה סוכם, ומה נשאר פתוח. הוא נוסף אוטומטית לכרטיס הלקוח.
הפרומפטים קפואים בייט-בייט כדי שיישבו מאחורי cache breakpoint. ופירוק הפלט של המודל הוא הגנתי בטירוף — חמישה שלבים, שאחד מהם מציל מחרוזות שלמות מתוך תשובה שנקטעה באמצע (והאחרון הוא פשוט פיצול לשורות). מודלים מחזירים JSON עטוף ב-code fence לעיתים קרובות מספיק כדי שזה ישתלם.
החלק המעניין: מאיפה משיגים דאטה
אני מאוד אוהב לבנות אייג'נטים שלא צריך לאמן בתהליך ארוך, אלא למצוא דאטה אמיתי ולדחוף אותו ל-Claude Code שיאמן את עצמו. צריך בקרה אנושית, אבל זה הרבה יותר קל כשיש דאטה.
אז חיפשתי דרך לייצא את כל השיחות מוואטסאפ ב-bulk. כמובן שאין. אז בניתי תוסף Chrome שני, שכל מטרתו להקליט: לוחצים start recording, נכנסים לשיחות אחת-אחת בזמן שהוא קורא ברקע, ובסיום יוצא קובץ JSON אחד. במקום לייצא צ'אט אחר צ'אט, כ-30 שניות לשיחה, זה ירד לשנייה לשיחה בערך — בתנאי אחד: הוא קולט רק מה ש-WhatsApp באמת רינדרו. אם צריך היסטוריה ישנה, גוללים למעלה בצ'אט לפני שעוצרים. זה לא ייצוא היסטוריה מלאה כמו הייצוא של WhatsApp עצמם.
מה שהיה מסובך בו זה לא ההקלטה אלא התפירה. WhatsApp מרנדרים רק את מה שנמצא בתצוגה — שורות ישנות נפרקות מהזיכרון כשגוללים מהן — אז חלון סריקה חדש הוא לא תמונה שלמה. המקליט ממזג חלונות חופפים לפי מזהי הודעות במקום להחליף אותם; וכשלחלון החדש אין ולו הודעה אחת משותפת עם מה שכבר נאסף, כיוון הגלילה מכריע אם זו היסטוריה ישנה או חדשה. זו היוריסטיקה, והיא מתועדת ככזאת.
עוד כמה דברים שהתבררו בדרך: כיוון ההודעה נקבע לפי אייקון ה-tail של הבועה, מה שעובד נכון גם בפריסת RTL. הסלקטורים נתלים על מטא-דאטה שוואטסאפ עצמם משתמשים בה ל-copy/paste, וזה ההוק היציב ביותר שמצאתי. וכבר אי אפשר לחלץ מספר טלפון: WhatsApp הפסיקו לשים JID בדף, אז מספר נקלט רק כשהוא מוצג כטקסט — כלומר רק לאיש קשר שאינו שמור.
הפקד עצמו יושב ב-shadow root סגור בתוך אלמנט בלי id ובלי class — לא התחכמות, פשוט popup של הדפדפן נסגר ברגע שלוחצים על צ'אט, וצריך פקד שנשאר על המסך בזמן שמדפדפים בין שיחות.
מה לא עובד
- הסלקטורים תלויים במבנה של WhatsApp, שמשתנה בלי הודעה. כשזה קורה, זה נשבר.
- טקסט בלבד. תמונות, הודעות קוליות וקבצים לא נקראים, וגם המקליט מדלג עליהם.
- ההצעות הן טיוטות. מודל שקיבל שיחה חלקית ימציא בביטחון מלאי, מחירים ותאריכי אספקה. לקרוא לפני ששולחים.
- בגרסה הציבורית החיבור לחנות מדומה, כדי שאפשר יהיה להריץ בלי credentials. המעבר לחנות אמיתית מבודד לפונקציית חיפוש אחת.
מה למדתי
אייג'נט לא חייב לדבר עם הלקוח. בינתיים זה עדיין מצ'וקמק. אבל הוא בהחלט יכול להעלות את רמת הביצועים של הנציג האנושי, וזה מסלול הטמעה הרבה יותר הדרגתי: העובדים מקבלים כלי עזר, המנהלים מקבלים בקרה, וכל אחד עדיין יכול "לקחת שליטה" מהטלפון.
אייג'נט טוב הוא אייג'נט מחובר להקשר. דאטה היסטורי בשביל ה"אימון" (שבסוף הוא כתיבת פרומפט, אבל יש כבוד), ודאטה בזמן אמת בשביל התשובה עצמה — במקרה הזה, המידע מ-Shopify.
לפעמים צריך להיות יצירתי כדי לחלץ את הדאטה. הכי קל להתעצל ולהגיד ללקוח שזה "תהליך שדורש אימון". בעיניי זה חלק מהתפקיד שלי כמטמיע — למצוא את הפתרון בעצמי, ולא להעביד את הלקוח.
אם יש לכם תהליך שנתקע בדיוק בנקודה הזאת — כלי שהעסק לא יכול לעבור אליו בלי לוותר על משהו שעובד — ככה אני ניגש לזה: איך אני עובד.