מרכזייה ישנה בלי API: מדריך סוכן התיקייה של VoiceBox
מרכזייה שאינה יודעת לקרוא לכתובת HTTP עדיין כותבת קבצים לתיקייה. הסוכן המקומי קורא את התיקייה הזו ומעלה כל הקלטה חדשה בבקשה חתומה - בלי לשנות דבר במרכזייה, ובלי לפתוח את הרשת שלכם החוצה.
כיוון אחד בלבד: מכם אלינו. הסוכן אינו מאזין לפורט, אינו מוחק קבצים ואינו דורש גישה מבחוץ.
- סריקה
- כל 60 שניות
- חלון גיל
- 7 ימים
- קובץ נחשב סגור
- אחרי 60 שניות
- העלאות במקביל
- 2
למי המדריך הזה, ומה הוא אינו מבטיח
למי שמפעיל מרכזייה ותיקה, מקליט מקומי או התקנת Asterisk שכותבת הקלטות לתיקייה, וצריך שההקלטות האלה יגיעו למקום מסודר - בלי להחליף את המרכזייה ובלי לפתח ממשק.
ומיד ההסתייגות החשובה: אין כאן תמיכה אוניברסלית ב-Asterisk, ואין הבטחה גורפת לכל התקנה שכותבת קבצים. הסוכן אינו מתחבר ל-AMI, ל-ARI או לבסיס נתוני ה-CDR, ואינו יודע דבר על תוכנית החיוג שלכם. הוא קורא קבצים ולומד את פרטי השיחה משם הקובץ. לכן השאלה המכריעה אינה איזו מרכזייה יש לכם, אלא איך נראים שמות הקבצים שהיא מייצרת: אם הם כוללים את מספר הצד השני ואת מועד השיחה, זה עובד. אם לא, צריך לשנות את תבנית השמות במרכזייה או להגדיר תבנית משלכם - ולפעמים התשובה הכנה היא שאין בשם מספיק מידע כדי לשייך שיחה לצרכן בביטחון.
ודבר אחד שלא נבקש מכם לעשות בשום שלב: לא לחשוף את תיקיית ההקלטות לאינטרנט. אין לנו משיכה מתיקיות, מדלי אחסון או משרת SFTP, ולכן אין שום סיבה לפתוח את השרת שלכם החוצה. כל התעבורה יוצאת מכם אלינו.
איך זה עובד, בשבע שורות
- הסוכן סורק את התיקייה שבמעקב, כולל תת-תיקיות, אחת לדקה.
- הוא מתעלם מקבצים שאינם אודיו, ומקבצים שזמן השינוי שלהם ישן מחלון הגיל.
- הוא בודק שהקובץ הפסיק לגדול, כדי לא לשלוח שיחה שעדיין מוקלטת.
- הוא קורא משם הקובץ מזהה, מספר, מועד, ולעיתים גם שלוחה וכיוון.
- הוא מעלה את הקובץ בבקשת
multipartחתומה ב-HMAC-SHA256. - הוא רושם בקובץ מצב מקומי מה כבר טופל, כדי לא לשלוח פעמיים.
- מה שלא הצליח מדווח ביומן - או לניסיון חוזר, או כדבר שדורש התערבות.
נקודת הקצה שאליה הוא מעלה היא בדיוק זו שמתועדת במדריך ה-Webhook וההעלאה, ולכן מי שמעדיף לכתוב את ההעלאה בעצמו במקום להריץ את הסוכן - יכול.
דרישות
- סביבת ריצה.
Node 20ומעלה. הסוכן משתמש בספריות התקן של Node בלבד, בלי תלויות מקומפלות, ולכן הוא רץ על Linux, Windows ו-macOS באותה צורה. קובץ המצב הוא JSON פשוט ולא בסיס נתונים, בדיוק כדי שלא יידרש רכיב שמתקמפל על מכונה בחדר שרתים שאיש אינו מתחזק. - גישה לתיקייה. הרשאת קריאה לתיקיית ההקלטות. המכונה אינה חייבת להיות המרכזייה עצמה - שיתוף רשת ממופה מספיק.
- יציאה החוצה. HTTPS בלבד, מכם אלינו. אין פורט שנפתח לכניסה.
- מקור קליטה. מזהה ומפתח חתימה, שמוצגים בקונסולה אחרי שהוקצה לכם מקור קליטה מסוג סוכן מקומי.
- אופן ההפצה. הסוכן אינו מפורסם כחבילה ציבורית ואין לו כרגע קובץ התקנה חתום ל-Windows. הוא נמסר יחד עם מקור הקליטה, כתיקיית קוד קטנה שרצה תחת Node. בדוגמאות כאן
voicebox-agentמייצג את פקודת ההפעלה שתקבלו איתו.
התקנה, שלב אחר שלב
- לבקש מקור קליטה מסוג סוכן מקומי. פתיחת חשבון אינה יוצרת מקורות קליטה. אנחנו מקצים מקור, ואחר כך אפשר להציג בקונסולה את המזהה ואת מפתח החתימה. כל חשיפה של מפתח נרשמת ביומן הביקורת.
- להכין משתמש ייעודי עם הרשאת קריאה בלבד. משתמש מערכת בלי הרשאת כתיבה לתיקיית ההקלטות, עם תיקייה אחת משלו לקובץ המצב. אין להריץ את הסוכן כמשתמש בעל הרשאות מלאות.
- להתקין Node 20 ומעלה על מכונה שרואה את התיקייה. לא חייבת להיות המרכזייה עצמה: כל מכונה עם גישת קריאה לתיקייה, כולל שיתוף רשת, ועם יכולת לפנות החוצה ב-HTTPS.
- לכתוב קובץ הגדרות ולהגן עליו. קובץ JSON עם כתובת הבסיס, מזהה מקור הקליטה, מפתח החתימה, התיקייה שבמעקב ואזור הזמן שבו המרכזייה כותבת את חותמות הזמן. הרשאת קריאה למשתמש הסוכן בלבד, כי בקובץ הזה יושב מפתח. הקונסולה מייצרת קובץ מוכן להדבקה, שבו נותר להחליף את התיקייה ולאשר את אזור הזמן.
- להריץ מעבר יחיד ולקרוא את היומן. הרצה עם הדגל --once סורקת, מעלה את מה שנמצא בחלון הגיל, כותבת את קובץ המצב ונעצרת. אפשר להקדים לה --dry-run, שמבצע את אותה סריקה ומדפיס מה היה נקלט ומאיזה מועד, בלי להעלות ובלי לכתוב דבר. זו ההרצה הראשונה הבטוחה.
- לאמת בקונסולה שהשיחה נקלטה עם האודיו. יש לוודא שהשיחה מופיעה, שיש לה הקלטה, ושהיא אינה מסומנת בבידוד. שורה בלי אודיו היא חיבור שלא הושלם.
- להפעיל את הסוכן כשירות מנוהל. שירות systemd או משימה מתוזמנת ב-Windows, עם הפעלה מחדש אוטומטית ועם איסוף הפלט ליומן שמישהו קורא.
קובץ ההגדרות
הסוכן מחפש קובץ הגדרות לפי הסדר הזה, והראשון שנקרא בהצלחה מנצח: הנתיב שאחרי --config=, המשתנה VOICEBOX_AGENT_CONFIG, הקובץ voicebox-agent.json בתיקייה הנוכחית, ולבסוף ~/.voicebox/agent.json.
{
"baseUrl": "https://voicebox.co.il",
"connectorId": "con_xxxxxxxxxxxxxxxxxxxxxx",
"secret": "READ_THIS_FROM_THE_ENVIRONMENT_IN_PRODUCTION",
"watchDir": "/var/spool/asterisk/monitor",
"intervalSeconds": 60,
"maxAgeDays": 7,
"concurrency": 2,
"defaultDirection": "outbound",
"statePath": "/var/lib/voicebox/agent-state.json",
// Which clock the timestamps in the file names were written in. Defaults to
// the machine's own zone; set it explicitly so a server rebuild cannot move
// every call by two hours without anyone noticing.
"timeZone": "Asia/Jerusalem",
// "allow" (default) uses the file's modification time when the name carries
// no timestamp, and labels it as derived. "reject" refuses instead - the
// right setting for a migrated archive, where the file date is the copy date.
"mtimeFallback": "allow"
}| מפתח | ברירת מחדל | משתנה סביבה | מה זה עושה |
|---|---|---|---|
baseUrl | חובה | VOICEBOX_BASE_URL | כתובת הבסיס שאליה מועלים הקבצים |
connectorId | חובה | VOICEBOX_CONNECTOR_ID | מזהה מקור הקליטה מהקונסולה |
secret | חובה | VOICEBOX_AGENT_SECRET | מפתח החתימה. עדיף במשתנה סביבה מאשר בקובץ |
watchDir | חובה | VOICEBOX_WATCH_DIR | התיקייה שבמעקב, כולל תת-התיקיות שלה |
intervalSeconds | 60 | VOICEBOX_INTERVAL | כמה זמן ממתינים בין מעבר למעבר |
maxAgeDays | 7 | VOICEBOX_MAX_AGE_DAYS | חלון הגיל, לפי זמן השינוי של הקובץ. קובץ ישן ממנו אינו נסרק כלל. 0 מבטל את המגבלה |
timeZone | אזור הזמן של המכונה | VOICEBOX_TIMEZONE | לפי איזה שעון נקראות חותמות הזמן שבשמות הקבצים |
mtimeFallback | allow | VOICEBOX_MTIME_FALLBACK | מה עושים כשאין מועד בשם: allow לוקח את זמן הקובץ ומסמן אותו כנגזר, reject דוחה |
since / until | — | VOICEBOX_SINCE, VOICEBOX_UNTIL | גבולות מפורשים לגל סריקה בתבנית yyyy-mm-dd, לפי זמן השינוי של הקובץ. גוברים על חלון הגיל |
maxDepth | 4 | VOICEBOX_MAX_DEPTH | עומק תת-התיקיות שנסרק. תיקייה עמוקה יותר מדווחת ביומן ואינה נסרקת |
concurrency | 2 | VOICEBOX_CONCURRENCY | כמה העלאות רצות במקביל |
statePath | ~/.voicebox/agent-state.json | VOICEBOX_STATE_PATH | איפה נשמר מה שכבר טופל |
defaultDirection | outbound | — | הכיוון כשאין לו רמז בשם הקובץ. מקובץ ההגדרות בלבד |
patterns | התבניות המובנות | — | תבניות שמות משלכם. מחליפות את המובנות, לא מתווספות אליהן |
שני מפתחות - defaultDirection ו-patterns - נקראים מקובץ ההגדרות בלבד ואין להם משתנה סביבה. אם חסר אחד מארבעת השדות הראשונים, הסוכן מדפיס בדיוק מה חסר ונעצר עם קוד יציאה 2, במקום לרוץ ולא לעשות כלום.
הרצה ראשונה
# Every flag, environment variable and config key, from the binary itself.
voicebox-agent --help
# Rehearse: same scan and same verdicts, nothing uploaded, no state written.
voicebox-agent --once --dry-run --config=/etc/voicebox/agent.json
# One pass, then exit. This is the safe first run: it scans, uploads what it
# finds inside the age window, writes the state file and stops.
VOICEBOX_AGENT_CONFIG=/etc/voicebox/agent.json voicebox-agent --once
# The same, naming the config file explicitly.
voicebox-agent --once --config=/etc/voicebox/agent.json--help מדפיס את רשימת הדגלים, משתני הסביבה ומפתחות ההגדרה כפי שהבינארי עצמו קורא אותם - זה המקור המוסמך, והטבלה כאן היא העתק שלו. עם --once מתבצע מעבר אחד והתהליך נעצר. זו ההרצה שכדאי להתחיל ממנה, וזו גם הצורה הנכונה להריץ מתוך cron או מתוך מתזמן המשימות של Windows. בלי הדגל הזה הסוכן נשאר לרוץ ומבצע מעבר בכל intervalSeconds.
הרצה כשירות
[Unit]
Description=VoiceBox folder agent
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=voicebox
Group=voicebox
Environment=VOICEBOX_AGENT_CONFIG=/etc/voicebox/agent.json
# Keep the signing key out of the config file on disk where you can:
# EnvironmentFile=/etc/voicebox/agent.env (chmod 600, contains VOICEBOX_AGENT_SECRET)
ExecStart=/opt/voicebox-agent/start.sh
Restart=always
RestartSec=30
# It reads recordings and writes one state file. Nothing else.
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
NoNewPrivileges=true
ReadWritePaths=/var/lib/voicebox
[Install]
WantedBy=multi-user.targetב-Windows: משימה מתוזמנת שמריצה את הסוכן עם --once כל כמה דקות, תחת חשבון שירות ייעודי. בשני המקרים חשוב שהפלט ייאסף לקובץ שמישהו קורא - סוכן ששותק כי הוא מת נראה בדיוק כמו סוכן ששותק כי אין שיחות חדשות.
תבניות שמות הקבצים
זה החלק שקובע אם ההתקנה שלכם מתאימה. הסוכן מסיר את הסיומת ובודק את השם מול התבניות לפי הסדר, מהספציפית לכללית, ועוצר בראשונה שמתאימה. מספר טלפון מזוהה כרצף של 6 עד 15 ספרות.
| תבנית | דוגמה סינתטית | מה נקרא ממנה |
|---|---|---|
| <in|out>-<שלוחה>-<מספר>-<yyyymmdd>-<hhmmss>[-<מזהה>] | out-101-0500001234-20270401-091500-abc.wav | כיוון יוצא, שלוחה 101, מספר 0500001234, 1.4.2027 בשעה 09:15 לפי אזור הזמן שהוגדר |
| <yyyy-mm-dd>_<hh-mm-ss>_<שלוחה>_<מספר> | 2027-04-01_11-15-00_agent7_0500001234.mp3 | שלוחה agent7, מספר 0500001234, 1.4.2027 בשעה 11:15; הכיוון לפי ברירת המחדל |
| <מספר>-<yyyymmddhhmmss> | 0500001234-20270401121500.wav | מספר 0500001234, 1.4.2027 בשעה 12:15; בלי שלוחה, הכיוון לפי ברירת המחדל |
| <שלוחה>_<מספר>, בלי חותמת זמן | 101_0500001234.wav | שלוחה 101, מספר 0500001234; אין מועד בשם - נלקח זמן השינוי של הקובץ ומסומן כמועד נגזר, או שהקובץ נדחה אם mtimeFallback=reject |
שם שאינו מתאים לאף תבנית אינו נשלח. הוא נרשם ביומן כ"שם הקובץ אינו מזוהה", ונשמר בקובץ המצב עם הסיבה unparseable-filename. זו החלטה מכוונת: ניחוש שגוי משייך שיחה לצרכן הלא נכון ומתגלה מאוחר מדי, ואילו שורה ביומן היא משהו שאפשר לתקן היום.
ארבעה פרטים שכדאי להכיר
- כיוון השיחה. קידומת
inאוoutבשם קובעת. בכל תבנית אחרת נלקחdefaultDirection, שברירת המחדל שלו היאoutbound. אם המרכזייה מקליטה בעיקר שיחות נכנסות, שנו את ברירת המחדל: הכיוון אינו קישוט, והחובה חלה גם על שיחה שהצרכן יזם. - אזור הזמן נקבע ונרשם. חותמת זמן בשם קובץ אינה נושאת אזור זמן. ברירת המחדל היא אזור הזמן של המכונה שמריצה את הסוכן - כמו קודם - אבל הוא נפתר פעם אחת בעלייה, נכתב ביומן, נשמר בקובץ המצב ונשלח עם כל העלאה. אפשר לקבע אותו ב-
timeZoneבקובץ ההגדרות או ב---timezone=, וכדאי: שרת שנבנה מחדש עם אזור זמן אחר יזיז אחרת כל שיחה בשעתיים או שלוש. מעברי שעון קיץ מטופלים - שעה שנדלגה או שחזרה פעמיים מסומנת בשדה נפרד ולא נופלת בשקט. - תאריך בלי שעה מייצר מועד של חצות באותו יום. זה מדויק ליום ולא לשעה, וכדאי לדעת את זה לפני שמסתמכים על השעה בדוחות.
- אין חותמת זמן בשם? נלקח זמן השינוי של הקובץ, נרשמת אזהרה ביומן, והמועד מסומן במפורש כ-
file-mtime- כלומר נגזר ולא נקרא. במרכזייה שכותבת את הקובץ בסוף השיחה זה קירוב סביר; בארכיון שהועתק זה תאריך ההעתקה, ולכן להעברת ארכיון ישmtimeFallback: "reject"שדוחה את הקובץ במקום לנחש.
תבנית משלכם
כשהתבניות המובנות אינן מתאימות אפשר להגדיר ביטויים רגולריים משלכם, עם קבוצות בעלות שם. הקבוצות שהסוכן קורא הן dir, agent, num, y, mo, d, h, mi, s ו-id. חובה שתהיה num: בלי מספר הצד השני אין למי לשייך את השיחה, וההתאמה נדחית גם אם הביטוי עצמו התאים. שימו לב שתבניות משלכם מחליפות את המובנות ולא מתווספות אליהן - אם עדיין צריך אחת מהן, יש לכתוב גם אותה.
{
"watchDir": "/var/spool/asterisk/monitor",
"patterns": [
{
"label": "site-<number>-<yyyymmdd>-<hhmmss>",
"regex": "^SITE~(?<num>\\d{6,15})~(?<y>\\d{4})(?<mo>\\d{2})(?<d>\\d{2})~(?<h>\\d{2})(?<mi>\\d{2})(?<s>\\d{2})$"
},
{
"label": "out-<agent>-<number>-<yyyymmdd>-<hhmmss>-<id>",
"regex": "^(?<dir>in|out)-(?<agent>[^-]+)-(?<num>\\d{6,15})-(?<y>\\d{4})(?<mo>\\d{2})(?<d>\\d{2})-(?<h>\\d{2})(?<mi>\\d{2})(?<s>\\d{2})(?:-(?<id>[\\w.]+))?$"
}
]
}מתי קובץ נחשב מוכן לשליחה
שיחה שעדיין מוקלטת היא קובץ שגדל. שליחה שלו מייצרת הקלטה קטועה שעוברת כל בדיקה - היא אודיו תקין, היא פשוט נגמרת מוקדם - וזו תקלה שמתגלה רק כשמישהו מבקש את השיחה. לכן יש שני סימנים, ומספיק שאחד מהם מתקיים:
- שקט של דקה. קובץ שלא נגעו בו 60 שניות נחשב סגור. זה הסימן היחיד שזמין להרצה עם
--once, שמתחילה בלי זיכרון ממעברים קודמים. - שתי מדידות באותו גודל. בהרצה מתמשכת, קובץ שנצפה פעמיים באותו גודל ושעברו לפחות 15 שניות מאז שינויו האחרון נחשב סגור. זה מזרז את הקליטה, ותופס גם כותב שמרענן את חותמת הזמן לעיתים רחוקות.
קובץ שגדל בין שתי מדידות אינו נשלח, גם אם חותמת הזמן שלו נראית ישנה. במרכזייה שמקליטה שיחות ארוכות זה בדיוק המנגנון שמונע חצי שיחה.
ההעלאה עצמה
כל קובץ נשלח בבקשת multipart/form-data אל POST /api/ingest/<connectorId>/upload, עם שדה meta ושדה audio. שתי כותרות מצורפות: x-voicebox-timestamp עם הזמן בשניות, ו-x-voicebox-signature עם HMAC-SHA256 על המחרוזת <timestamp>.<meta>.
שדה meta מכיל externalId - שם הקובץ בלי הסיומת - startedAt, counterparty, agentRef אם נמצא, direction ו-sourceFilename, וכן startedAtSource (filename או file-mtime), timeZone ו-sha256 של הקובץ כפי שחושב אצלכם. שלושת האחרונים נשמרים עם השיחה, כך שאפשר לדעת בדיעבד לפי איזה שעון נקרא המועד ומה מקורו. משך השיחה אינו נשלח: הוא נמדד מהקובץ אצלנו. קובץ שאין לו מועד שיחה כלל אינו נשלח - הסוכן מעדיף לדחות מאשר לתייג שיחה בתאריך של היום.
החתימה מכסה את שדה meta בלבד ולא את בייטים של האודיו; אלה מוגנים בהעברה על ידי TLS. בתשובה מוחזר SHA-256 של מה שהתקבל. מגבלת הגודל היא 200MiB לבקשה כולה, וההעלאה נקטעת אם לא הסתיימה בתוך שתי דקות.
תגובת השרת מתורגמת לשלוש התנהגויות: הצלחה נרשמת כטופלה; תקלת רשת, 429 או 5xx אינן נרשמות כלל, כך שהמעבר הבא ינסה שוב; וכל 4xx אחרת נרשמת כנדחתה ולא תנוסה שוב עד שתתערבו. זו החלטה מכוונת - ניסיון חוזר אינסופי על מפתח שגוי רק קובר את התקלה האמיתית מתחת ליומן מלא בשגיאות זהות.
בידוד
הסוכן אינו מחליט מה מבודד; השרת מחליט, והסוכן מדווח. שני מקרים מסומנים בבידוד: קובץ שאינו אודיו בפורמט מזוהה, ו-WAV מסוג PCM ששקט לחלוטין. שניהם הסימפטום הקלאסי של ערוץ הקלטה שאינו מחובר כראוי.
קובץ מבודד נשמר ואינו נזרק, ההקלטה מסומנת ככזו בקונסולה, והסוכן רושם ביומן שורת אזהרה עם ההסבר שהתקבל מהשרת. הוא לא ינסה לשלוח אותו שוב - מבחינתו הקובץ טופל. אחרי תיקון הבעיה במרכזייה, הדרך להעלות את אותו קובץ מחדש היא למחוק את הרשומה שלו מקובץ המצב.
קובץ המצב
קובץ JSON אחד, שממפה נתיב יחסי בתוך התיקייה שבמעקב אל מה שקרה לו. הוא נכתב לקובץ זמני ואז מוחלף בשמו הסופי, כך שהפסקת חשמל באמצע כתיבה משאירה את הקודם שלם במקום קובץ חצי כתוב.
{
"2027/04/out-101-0500001234-20270401-091500-abc.wav": {
"state": "uploaded",
"size": 1841200,
"mtimeMs": 1806302103000,
"sha256": "5cecb854777c67598906af...",
"startedAtSource": "filename",
"timeZone": "Asia/Jerusalem",
"callId": "cal_7q4m2xk9vd3hbn5r8ts0jw",
"serverCreated": true,
"at": "2027-04-01T06:15:04.118Z"
},
"2027/04/out-101-0500001234-20270401-091500-abc_copy.wav": {
"state": "duplicate",
"size": 1841200,
"mtimeMs": 1806302140000,
"sha256": "5cecb854777c67598906af...",
"duplicateOf": "2027/04/out-101-0500001234-20270401-091500-abc.wav",
"at": "2027-04-01T06:15:04.410Z"
},
"recording-final-v2.wav": {
"state": "rejected",
"size": 92160,
"mtimeMs": 1806301500000,
"reason": "unparseable-filename",
"at": "2027-04-01T06:15:04.771Z"
}
}- קובץ נחשב מטופל רק אם המפתח, הגודל וזמן השינוי תואמים לרשום. קובץ שהוחלף בגרסה אחרת ייסרק שוב.
- מחיקת קובץ המצב גורמת לסריקה מלאה מחדש. זה בטוח מבחינת כפילויות, כי המזהה החיצוני הוא שם הקובץ והעלאה חוזרת מתמזגת עם השיחה הקיימת - אבל היא כן מעלה שוב את כל האודיו שבחלון הגיל.
- לצד המפתח נשמר גם
SHA-256של התוכן, וממנו נבנה בעלייה אינדקס תוכן בזיכרון. קובץ שתוכנו כבר נקלט תחת שם אחר נרשם כ-duplicateעם הפניה למקור ואינו נשלח. הגיבוב מחושב רק לקבצים שעברו את הבדיקה הזולה - מפתח, גודל וזמן שינוי - ולכן ארכיון שכבר טופל אינו נקרא שוב. - קובץ מצב ישן, מלפני שהגיבוב נשמר, נטען כרגיל. ההגנה מפני כפילות תוכן חלה על מה שנסרק מכאן והלאה ולא רטרואקטיבית.
- קובץ מצב פגום נטען כריק, עם אזהרה ביומן. עלות התקלה היא סריקה כפולה, ולא סוכן שלא עולה.
- רשומה מסוג
rejected- שם שלא פוענח, או שגיאה קבועה מהשרת - חוסמת ניסיון נוסף. מחיקת הרשומה היא הדרך לבקש ניסיון חוזר.
# Ask for one file to be tried again, after fixing whatever rejected it:
# drop its entry from the state file while the agent is stopped.
systemctl stop voicebox-agent
python3 - <<'PY'
import json, pathlib
path = pathlib.Path('/var/lib/voicebox/agent-state.json')
state = json.loads(path.read_text())
state.pop('2027/04/out-101-0500001234-20270401-091500-abc.wav', None)
path.write_text(json.dumps(state))
PY
systemctl start voicebox-agentחלון הגיל, ומה זה אומר על מילוי היסטוריה
ברירת המחדל היא שבעה ימים, לפי זמן השינוי של הקובץ ולא לפי התאריך שבשם. המשמעות המעשית: הרצה ראשונה לא תעלה ארכיון ישן. זה מכוון - סוכן שמעלה בהתקנה עשור של הקלטות מייצר חשבון אחסון שאיש לא אישר, ומעמיס תור שחוסם את השיחות של היום.
החלון ניתן לשינוי, ובכוונה לא בשקט: הוא נכתב ביומן בכל עלייה, יחד עם ההערה שהוא נמדד לפי תאריך הקובץ. --max-age-days=0 מבטל את הגבול התחתון לגמרי, ו---since עם --until מגדירים גל אחד. --dry-run מריץ את אותה סריקה בדיוק, מדפיס מה היה נקלט ומאיזה מועד, ואינו מעלה דבר ואינו כותב לקובץ המצב - חזרה יבשה שאפשר להריץ שוב ושוב. כשבכל זאת צריך היסטוריה, מריצים מעבר יזום עם חלון רחב יותר, אחרי שסופרים מה ייכלל בו:
# Count first. Everything inside the wider window that has not been uploaded
# yet will be uploaded, and the retention clock runs from each call's own time,
# not from the day you backfilled it.
find /var/spool/asterisk/monitor -name '*.wav' -mtime -30 | wc -l
# Rehearse: same scan, same verdicts, nothing uploaded and no state written.
voicebox-agent --once --dry-run --max-age-days=30 --config=/etc/voicebox/agent.json
# Then do it. One deliberate pass, then back to the service and its 7-day default.
voicebox-agent --once --max-age-days=30 --config=/etc/voicebox/agent.json
# A whole archive, with the age window switched off entirely (0 = no lower bound),
# or one wave at a time by file date:
voicebox-agent --once --max-age-days=0 --timezone=Asia/Jerusalem --mtime-fallback=reject
voicebox-agent --once --max-age-days=0 --since=2026-01-01 --until=2026-06-30שלוש הערות לפני שמרחיבים את החלון. ראשית, זמן השינוי של קובץ שהועתק או שוחזר מגיבוי הוא זמן ההעתקה, ולכן ארכיון ששוחזר עלול להיראות כולו "חדש". שנית, תקופת השמירה נמדדת ממועד השיחה עצמה, ולכן שיחה ישנה שנקלטת היום עשויה להיות קרובה לסוף התקופה שלה או מעבר לה. שלישית, בתוך כל מעבר הסוכן מעלה מהישן לחדש, כדי שמה שקרוב למועד יטופל קודם. ורביעית, חלון שנפתח לצורך מילוי היסטוריה צריך להיסגר אחריו: חלון פתוח שנשאר פתוח סורק את כל הארכיון בכל מעבר.
להעברה של ארכיון שלם, ולא רק להשלמת ימים אחדים, יש מדריך מיגרציה נפרד - עם נוהל, גיליון עבודה וספירת התאמה. שם גם מוסבר למה בארכיון שהועתק כדאי להריץ עם mtimeFallback: "reject" ועם אזור זמן מפורש.
הרשאות מצומצמות ואבטחה
- קריאה בלבד לתיקיית ההקלטות. הסוכן אינו מוחק, אינו משנה ואינו מזיז דבר; הוא כותב רק את קובץ המצב, ולכן זו התיקייה היחידה שדורשת הרשאת כתיבה.
- משתמש ייעודי, לא root ולא Administrator. יחידת ה-systemd שלמעלה מצמצמת גם את מה שהתהליך רואה במערכת הקבצים.
- המפתח הוא סוד. עדיף במשתנה סביבה של השירות; בקובץ - עם הרשאות למשתמש הסוכן בלבד. לא בגיט, לא בדוא״ל, ולא כארגומנט בשורת הפקודה, שנראה לכל מי שמריץ רשימת תהליכים.
- אין כניסה מבחוץ. הסוכן אינו מאזין לפורט, ונדרש חוק חומת אש ליציאה בלבד.
- החלפת מפתח מבטלת מיד את הקודם, וכל סוכן שממשיך עם הישן נעצר. לכן מחליפים בחלון תחזוקה ומעדכנים את ההגדרות באותו מהלך.
ניטור: מה לבדוק, וממה להתריע
הסוכן כותב שורות יומן עם חותמת זמן ורמה, ומדפיס שורת סיכום רק כשהיה מה לדווח. שקט אינו בהכרח בעיה - אבל שקט של יומיים באמצע שבוע עבודה כן.
2027-04-01T06:15:03.201Z [info] config: /etc/voicebox/agent.json
2027-04-01T06:15:03.204Z [info] סוכן VoiceBox מופעל → https://voicebox.co.il
2027-04-01T06:15:03.204Z [info] תיקייה במעקב: /var/spool/asterisk/monitor
2027-04-01T06:15:03.205Z [info] חותמות זמן בשמות קבצים נקראות לפי אזור: Asia/Jerusalem
2027-04-01T06:15:03.205Z [info] חלון הסריקה: 7 ימים אחרונים (לפי תאריך הקובץ)
2027-04-01T06:15:04.118Z [info] הועלה: 2027/04/out-101-0500001234-20270401-091500-abc.wav cal_7q4m2xk9vd3hbn5r8ts0jw
2027-04-01T06:15:04.410Z [info] כפילות תוכן, לא הועלה: 2027/04/out-101-0500001234-20270401-091500-abc_copy.wav = 2027/04/out-101-0500001234-20270401-091500-abc.wav
2027-04-01T06:15:04.590Z [warn] הועבר לבידוד: 2027/04/in-104-0500005678-20270401-093000.wav
2027-04-01T06:15:04.771Z [warn] שם הקובץ אינו מזוהה: recording-final-v2.wav
2027-04-01T06:15:04.772Z [info] קבצים שנסרקו: ↑2 ✗1 ⧉1 =38- ביומן: שורות
הועלהבשעות הפעילות, בלי שורותנכשל (retry)שחוזרות שוב ושוב, ובלי מספר גדל שלשם הקובץ אינו מזוהה. - בקובץ המצב: כמה רשומות
rejectedו-quarantinedיש. עלייה בהן היא כמעט תמיד שינוי תצורה במרכזייה. - בשירות: שהתהליך חי, ושאינו נכנס ללולאת הפעלות מחדש.
- בקונסולה: מסך מקורות קליטה מציג את מצב המקור. מקור שסומן כתקול או כפעיל חלקית מצביע על כשלי חתימה, ולא על תקלה בתיקייה.
- ההתראה שהכי כדאי להקים: אף העלאה מוצלחת בשעות פעילות במשך כמה שעות. היא תופסת גם דיסק שהתמלא, גם שיתוף רשת שנותק וגם מרכזייה שהפסיקה להקליט.
תקלות נפוצות
| מה רואים | הסיבה הסבירה | מה עושים |
|---|---|---|
| הסוכן נעצר מיד, עם קוד יציאה 2 והודעה על פרטי הגדרה חסרים | לא נמצאו baseUrl, connectorId, secret או watchDir - לא בקובץ ההגדרות ולא במשתני הסביבה. | לוודא שקובץ ההגדרות נמצא באחד המקומות שהסוכן מחפש בהם, ושהמשתמש שמריץ אותו יכול לקרוא אותו. |
| כל העלאה נכשלת ב-401 | מפתח חתימה שגוי, מפתח של מקור קליטה אחר, או שעון שסוטה ביותר מחמש דקות. | להשוות את המפתח מול הקונסולה ולסנכרן שעון ב-NTP. שתי התקלות נראות זהות מבחוץ. |
| העלאות נכשלות ב-403 | מקור הקליטה מושבת. | מקור מושבת אינו נפתח מהקונסולה. יש לפנות אלינו. |
| העלאות נכשלות ב-404 | מזהה מקור הקליטה שבהגדרות אינו מוכר. | להעתיק מחדש את המזהה מהקונסולה, ולוודא שלא נדבקו אליו רווחים. |
| העלאה נכשלת ב-413 | הבקשה כולה מעל 200MiB. הקלטה ארוכה ב-WAV לא דחוס מגיעה לשם. | להגדיר במרכזייה קידוד דחוס להקלטות ארוכות. אין הרכבה מחלקים בצד שלנו. |
| הרבה שורות "שם הקובץ אינו מזוהה" | תבנית השמות של המרכזייה אינה תואמת לאף תבנית מובנית. | להגדיר תבנית משלכם, ואז למחוק את הרשומות שנדחו מקובץ המצב כדי שהקבצים ייסרקו שוב. |
| קבצים מדווחים כמבודדים | השרת קיבל קובץ שאינו אודיו בפורמט מזוהה, או WAV מסוג PCM ששקט לחלוטין. | זו כמעט תמיד תקלה בערוץ ההקלטה. כדאי להאזין לקובץ המקורי בתיקייה לפני שמחפשים במקום אחר. |
| שיחות ישנות אינן עולות | חלון הגיל: שבעה ימים לפי זמן השינוי של הקובץ. | להריץ מעבר יזום עם חלון רחב יותר - --max-age-days=30, או 0 לכל הארכיון, או --since ו---until לגל אחד - אחרי שסופרים כמה קבצים ייכללו בו. |
| שום דבר לא עולה, ואין שגיאות ביומן | אין קבצים חדשים, או שכל מה שיש כבר רשום בקובץ המצב. | הסוכן מדפיס שורת סיכום רק כשהיה מה לדווח. לבדוק את קובץ המצב ואת שעת השינוי של הקובץ האחרון. |
איך יודעים שזה עובד
- ביומן הסוכן יש שורת "הועלה" עם מזהה שיחה.
- בקונסולה השיחה מופיעה עם המספר והמועד שמופיעים בשם הקובץ.
- לשיחה יש הקלטה, והיא אינה מסומנת בבידוד.
- הרצה חוזרת אינה מעלה את אותו קובץ שוב ואינה מייצרת שיחה כפולה.
- אחרי יומיים, מספר השיחות ביום בקונסולה תואם למספר הקבצים שנוצרו בתיקייה באותו יום.
הבדיקה החמישית היא היחידה שמוכיחה כיסוי ולא רק חיבור: שיחה בודדת מוכיחה שהצינור פתוח, והשוואה יומית מוכיחה שהוא לא נסתם באמצע.
הסרה
- לעצור את השירות ולבטל את ההפעלה האוטומטית שלו.
- למחוק את יחידת השירות או את המשימה המתוזמנת.
- למחוק את קובץ ההגדרות ואת קובץ המצב. קובץ ההגדרות מכיל מפתח חתימה, ולכן מחיקתו היא חלק מההסרה ולא ניקיון אופציונלי.
- לבקש מאיתנו להשבית את מקור הקליטה, או להחליף את המפתח בקונסולה כדי לבטל מיד את הישן. מקור מושבת אינו נפתח מחדש מהקונסולה.
- ההקלטות שכבר נקלטו נשארות אצלנו וממשיכות להתנהל לפי מדיניות השמירה. הסרת הסוכן עוצרת קליטה, ואינה מוחקת דבר שכבר נשמר.
מה הסוכן אינו עושה
- אינו מתחבר למרכזייה, ל-AMI, ל-ARI או לבסיס נתוני CDR.
- אינו יודע על שיחה שלא הוקלטה - הוא רואה קבצים, לא שיחות.
- אינו ממיר פורמטים ואינו דוחס; הוא מעלה את הקובץ כפי שהוא.
- אינו מוחק, אינו מזיז ואינו משנה קבצים בתיקייה.
- אינו מושך מ-SFTP, מדלי S3 או מכל מקור מרוחק - רק ממערכת הקבצים שהוא רואה.
- אינו יורד מעבר לארבע רמות של תת-תיקיות, ואינו נכנס לתיקיות שמתחילות בנקודה.
- אינו קולט קבצים בסיומות שאינן ברשימת האודיו שהוא מכיר:
wav · mp3 · ogg · m4a · gsm · wma · aac · flac.
גרסה ושינויים
- גרסה 1.1 · 28 ביולי 2026
- פרסום ראשון: דרישות, התקנה, קובץ ההגדרות, תבניות שמות הקבצים, בדיקת יציבות, ההעלאה החתומה, בידוד, קובץ המצב, חלון הגיל ומילוי היסטוריה, הרשאות, ניטור, תקלות נפוצות והסרה. כל מה שכתוב כאן נבדק מול הרצה של הסוכן על תיקיית הקלטות סינתטית ומול מערך הבדיקות שלו.
אם התבנית שלכם אינה ברשימה
שלחו לנו חמישה שמות קבצים אמיתיים - את השמות בלבד, לא את הקבצים - ל-hello@voicebox.co.il, ונחזיר תבנית מוכנה או תשובה כנה שאין בשם מספיק מידע. מה שלא כדאי לעשות הוא למתוח את הביטוי עד ש"תופס משהו": תבנית שמתאימה בקלות רבה מדי משייכת שיחות לצרכן הלא נכון, וזה מתגלה בדיוק ברגע שבו הכי יקר לגלות.
מסלולי הקליטה האחרים, כולל מה שאינו נתמך היום, מרוכזים בעמוד האינטגרציות. מי שהמרכזייה שלו כן יודעת לקרוא לכתובת HTTP ימצא את התיעוד המלא במדריך ה-Webhook. התשלום החודשי על השירות שהסוכן מעלה אליו, ומה שאינו כלול בו, מפורטים בעמוד המחירים.
שאלות שחוזרות
- האם הסוכן תומך בכל התקנת Asterisk?
- לא, ואין דרך להבטיח זאת. הסוכן אינו מתחבר ל-AMI, ל-ARI או לבסיס נתוני ה-CDR ואינו יודע דבר על תוכנית החיוג שלכם. הוא קורא קבצים מתיקייה ולומד את פרטי השיחה משם הקובץ, ולכן מה שקובע הוא תבנית השמות שההתקנה שלכם מייצרת - למשל דרך MixMonitor. כמה תבניות נפוצות נתמכות מראש, ולכל תבנית אחרת אפשר להגדיר ביטוי משלכם, בתנאי שהשם מכיל את מספר הצד השני.
- האם צריך לפתוח את השרת או את תיקיית ההקלטות לאינטרנט?
- לא, ואין לעשות זאת. הסוכן יוצר חיבור יוצא ב-HTTPS מהרשת שלכם אלינו ואינו מאזין לשום פורט. תיקיית הקלטות שחשופה לאינטרנט היא דליפה שממתינה לקרות, ואנחנו ממילא לא מושכים מתיקיות: הכיוון היחיד שקיים הוא מכם אלינו.
- מה קורה אם הסוכן היה כבוי יומיים?
- במעבר הראשון אחרי ההפעלה הוא סורק את התיקייה, מדלג על מה שכבר רשום בקובץ המצב ומעלה את מה שנשאר, מהישן לחדש, כדי שהשיחות הקרובות ביותר למועד שמירה או מסירה ייקלטו קודם. מה שיצא בינתיים מחלון הגיל לא ייסרק, ולכן אחרי היעדרות ארוכה כדאי מעבר יזום עם חלון רחב יותר.
- מה קורה אם אותו קובץ נשלח פעמיים?
- לא נוצרת שיחה כפולה, ויש לכך שתי שכבות. בצד הסוכן: לפני כל העלאה מחושב SHA-256 של הקובץ, וקובץ שתוכנו כבר נקלט בשם אחר נרשם כ-duplicate ואינו נשלח - כך ששני עותקים של אותה שיחה בשני שמות אינם הופכים לשתי רשומות. בצד השרת: המזהה החיצוני הוא שם הקובץ בלי הסיומת, ושליחה חוזרת של אותו שם מתמזגת עם ההקלטה הקיימת. השרת מדווח בתשובה אם נוצרה רשומה חדשה או שהתבצע מיזוג, והסוכן רושם את זה ביומן.
- האם הסוכן מוחק או משנה קבצים בתיקייה?
- לא. הוא פותח אותם לקריאה בלבד וכותב רק את קובץ המצב שלו, במקום נפרד. מדיניות המחיקה בתיקייה נשארת שלכם, ומדיניות השמירה שלנו חלה על העותק שנקלט אצלנו.
- מה בדיוק נשלח בכל העלאה?
- שדה meta עם מזהה חיצוני, מועד תחילת השיחה, מספר הצד השני, שלוחת הנציג אם נמצאה, כיוון השיחה ושם הקובץ המקורי - ולצידם מקור המועד (שם הקובץ או תאריך הקובץ), אזור הזמן שבו נקרא, וה-SHA-256 שחושב אצלכם - וקובץ האודיו עצמו. החתימה מכסה את שדה meta בלבד ולא את בייטים של האודיו, שמוגנים בהעברה על ידי TLS. משך השיחה אינו נשלח: הוא נמדד מהקובץ אצלנו.
מרכזייה ותיקה היא בסוף שאלה של שמות קבצים
שלחו לנו חמישה שמות קבצים מהתיקייה שלכם, ונאמר אם הם נקראים כמו שהם, אם צריך תבנית משלכם, או אם חסר בהם מידע שאי אפשר להשלים. hello@voicebox.co.il
הסוכן מעלה לארגון מסוים, ולכן צריך חשבון לפני שיש לאן להעלות.
פתיחת חשבון דורשת שם עסק, דוא״ל וסיסמה, וחיבור מקור ההקלטות מוגדר בשלב נפרד אחריה. כל המחשבונים, התבניות וקובצי ההורדה באתר זמינים בלי חשבון.
להמשך קריאה
- Webhook ו-API להעלאת הקלטותתיעוד טכני לחיבור מרכזייה או CRM ל-VoiceBox: Webhook חתום, העלאת קובץ אודיו, שדות חובה, פורמטים נתמכים, מניעת כפילויות, שגיאות ודוגמאות curl ו-Node.
- אינטגרציותארבעה מסלולי קליטה נתמכים ומאומתים, מה שאינו נתמך היום - כולל משיכה מ-S3 ומ-SFTP - ובודק התאמה שרץ בדפדפן ואומר איזה מסלול מתאים למרכזייה שלכם.
- ביקורת כיסוי הקלטותנוהל ביקורת בשבעה שלבים: לייצא רשומות שיחה, להצליב מול ההקלטות, לחשב יחס כיסוי ולסווג כל פער. כולל תבנית CSV להורדה ודוגמה מסונתזת מלאה.