דלגו לתוכן

גשר צ'אט

ספק ערך תעבורה תעבורה סוג מרחב
Slack slack Socket Mode (WSS יוצא) slack

Slack הוא הספק היחיד. ChatProvider הוא enum בעל ערך יחיד ו-cc_server רושם תוסף אחד. הסכימה, כל חיפוש וכל אינדקס ייחודי ממופתחים לפי provider, כך שמזהים לעולם לא חוצים ספקים והוספת ספק היא שינוי בצד השרת בלבד.

אפליקציה אחת בצד הספק לכל סביבת עבודה × ספק.

שם הפקודה הוא לפי אפליקציה (cc כברירת מחדל, ולכן /cc). מנהל יכול לשנות את שמו; הגשר משתמש אז במה שאיתו הופעל לאחרונה.

פקודה השפעה
/cc help (או /cc בלי פועל) מפרט מה הבוט יודע לעשות
/cc link <code> מממש קוד חד־פעמי ומקשר את זהות הצ’אט של השולח
/cc ticket <title> פותח כרטיס מקומי שמדווח על ידי החבר המקושר
/cc ticket <title> | <description> אותו דבר, כשכל מה שאחרי ה-| הראשון הוא התיאור
פועל לא מוכר מדווח על הפועל הלא מוכר יחד עם טקסט העזרה

דרכים להגיע לסוכן:

מחווה התנהגות
@bot … בערוץ פותח שרשור ב-Slack ומרחב Control Center תואם; הסוכן משיב בשרשור ה-Slack
תשובה בשרשור Slack מגושר ממשיכה את אותו מרחב Control Center — בלי צורך באזכור
הודעה רגילה בערוץ מתעלמים ממנה (לא מכוונת לבוט ולא בתוך שרשור Slack מגושר)
הודעה ישירה לבוט מרחב Control Center אחד לכל שיחת הודעה ישירה; מגדיר כותרת לשרשור ה-Slack וסטטוס “is thinking…” בזמן שהסוכן עובד

מוצהרים על ידי מתאר הספק, שהוא זה שמייצר את דיאלוג החיבור ומאמת את השליחה.

מזהה שדה תווית נדרש קידומת היכן הוא נמצא
botToken אסימון גישה של הבוט כן xoxb- OAuth & Permissions
appToken אסימון גישה ברמת האפליקציה כן xapp- Basic Information → App-Level Tokens, הרשאה connections:write
configRefreshToken אסימון גישה לתצורת האפליקציה לא xoxe אסימוני תצורת אפליקציה ב-api.slack.com/apps

configRefreshToken הוא פרט הגישה לניהול האפליקציה: בלעדיו החיבור עובד, אבל אי אפשר ליצור או לערוך את הבוט מ-Control Center. הוא מתחלף בכל שימוש והערך המתחלף נשמר מיד.

הערכים נחתכים מרווחים מיותרים, מפתחות שלא הוצהרו נזרקים, ואי־התאמה בקידומת נדחית באותה מחרוזת גם בלקוח וגם בשרת. שדות נדרשים חייבים להיות נוכחים בכל חיבור; שדה אופציונלי שנשאר ריק שומר על מה שכבר היה שמור.

הרשאות אסימון הבוט:

app_mentions:read
assistant:write
channels:history
chat:write
commands
groups:history
im:history
users:read
users:read.email

מינויים לאירועי הבוט:

app_home_opened
app_mention
message.channels
message.groups
message.im

הגדרות מבניות ש-Control Center כופה כשהוא יוצר או עורך את האפליקציה: Socket Mode מופעל, בלי כתובת URL של בקשה, לשונית Messages מופעלת וניתנת לכתיבה, always_online לבוט, ההרשאות והאירועים הנדרשים ממוזגים אל מה שכבר יש לאפליקציה (ולעולם לא נגרעים ממנו), ופקודת קו נטוי אחת שנושאת את התיאור File a ticket or link your Control Center account — הסימן שמזהה את הפקודה של Control Center עצמו ב-manifest מיוצא.

מצב ערך תעבורה משמעות
לא מחובר disconnected לא נשמרו פרטי גישה עבור סביבת עבודה × ספק זו
מתחבר… connecting פרטי הגישה נשמרו, התעבורה מחייגת או מחייגת מחדש
פעיל connected התעבורה פעילה ומקבלת אירועים
שגיאת חיבור error נשמרו אבל נכשלים; השגיאה של הספק עצמו מוצגת לצידם

ההתחברות מחדש היא השהיה מעריכית עם jitter, שמוגבלת ל-30 שניות. סבב החלפת socket שיזם הספק מתחבר מחדש מיידית בלי להחמיר את ההשהיה. פרט גישה שנדחה (invalid_auth, not_authed, token_revoked, account_inactive, token_expired, invalid_app_id) או הרשאה חסרה הם סוף הדרך: הגשר מפסיק לנסות שוב עד שמגיעים פרטי גישה חדשים.

הגשר נכתב מול המשטח העשיר ביותר ומדרדר יכולת אחת בכל פעם.

יכולת Slack השפעה כשהיא חסרה
streaming כן התשובה כולה מפורסמת פעם אחת, בסוף
streamingRequiresThread כן יעד תשובה בלי עוגן שרשור מפרסם תשובות שלמות
ephemeralMessages כן תשובה לפקודת קו נטוי בלי ידית תשובה שמישה נזרקת (סירובים תמיד מפורסמים בתוך השרשור)
threadStatus כן אין שורה חולפת של “is thinking…”
threadTitle כן שרשור הצ’אט שומר על כותרת ברירת המחדל שלו
slashCommands כן אין משטח /cc
taskCards כן טקסט בלבד — בלי כרטיס מעל התשובה ובלי קישור “View in Control Center”
maxMessageLength 39,000 תשובות ארוכות יותר נחתכות עם הפניה ל-Control Center
maxStreamChunkLength 3,800 תוספות מפוצלות בגבול של פסקה, שורה או רווח

הצהרה על הזרמה אינה הבטחה להזרמה: זו של Slack דורשת את התכונה Agents & AI Apps בתוכנית בתשלום. סירוב בזמן הקריאה (streaming_not_supported, feature_not_enabled, paid_only, …) נזכר לכל חיי החיבור ומוצג כ-streamingAvailable: false. כרטיסי המשימה של Slack נוסעים על אותה קריאה, ולכן Slack שמסרב למצב התצוגה של הכרטיס שומר על הזרם שלו ומאבד רק את הכרטיס, פעם אחת, לכל חיי החיבור.

זרם גם צריך לנקוב בקורא שלו. Slack דורש גם משתמש נמען וגם את הצוות שלו כדי לפתוח אחד בערוץ (הודעה ישירה לא) ומסרב עם missing_recipient_user_id / missing_recipient_team_id אחרת — סירוב לפי קריאה, ולכן הגשר ממשיך לנסות ובינתיים התור מגיע כתשובה אחת מפורסמת. הנמען הוא מי ששאל, נלקח מההודעה הנכנסת (הצוות שלו, לא של האפליקציה, כך שערוץ Slack Connect נפתר נכון). לתור שאיש לא התחיל מהצ’אט — עבודה שהופעלה ב-Control Center על שיחה שכבר מגושרת — זה החבר שקישורו יצר את הגשר; אם הוא ביטל מאז את הקישור של חשבון הצ’אט שלו, אותו תור מפורסם שלם במקום להיות מוזרם.

כרטיס מקובץ אחד לכל תור של סוכן (task_display_mode: plan), עם שורה לכל שלב. כל שורה היא task_update של id + title + status, ובנוסף details חד־פעמי כשהמחשבה מוכנה. details ו-output עם אותו מזהה משורשרים בלי מפריד, ולכן details נשלחים פעם אחת ו-output לעולם אינו מוזרם. הכותרת של התוכנית היא הבקשה; התשובה היא תוספת נפרדת ומאוחרת יותר שנושאת מקטע markdown_text.

חלק תוכן
כותרת התוכנית מה שהתבקש
שורת הקמה Working on it…, אחר כך Cloning …, אחר כך Starting the agent… — שורה אחת, הכותרת מוחלפת
שורת חשיבה Thinking…, נוספת כשהסוכן מתחיל לחשוב; המחשבה היא ה-details של השורה הזו, נשלחת פעם אחת כשהחשיבה מסתיימת
שורות כלים אחת לכל כלי (Bash which swift), מזהה ייחודי כך שהן יושבות זו לצד זו
סטטוס בתהליך בזמן שהשורה הזו רצה; הושלם כשהשורה הבאה מתחילה או שהתור מסתיים; שגיאה במקרה של כשל, הפסקה או הגעה למגבלת התור
קריאה לפעולה View in Control Center כשורה אחרונה כשהתור מסתיים — נעדרת כשהעבודה עדיין רצה ונעדרת כשהלשרת אין כתובת ציבורית נגישה

מה שכרטיס לעולם אינו נושא: פלט כלים, ארגומנט שמחוץ לרשימת ההיתר הזו, או כל דבר שנקרא ממסד הנתונים. פרוזת החשיבה היא רק ה-details של שורת ה-Thinking… (מוגבלת ל-256 תווים), לא גוף ההודעה. כרטיס שלא יאמר דבר חדש אינו נשלח שוב. אזכור פותח את הזרם מיד כדי ששרשור ה-Slack לא יישאר שקט; תור שלא הפיק אחר כך שום טקסט מסיים את אותו כרטיס בהערה קצרה במקום לפרסם הודעה שנייה. תור בלי מילים במרחב שמעולם לא פתח זרם (אין כרטיסים, או שההזרמה כבר נדחתה) עדיין מפרסם הודעה אחת שנושאת את הכרטיס המוגמר ואת ההערה.

כרטיסים מוזרמים ומפורסמים הם מטענים שונים:

צורה מבנה פרוזה
מוזרם plan_update בתוספת מקטעי task_update שממופתחים לפי id מחרוזת פשוטה, מוגבלת ל-256 תווים לכל מקטע
מפורסם בלוק plan שעוטף task_card-ים שממופתחים לפי task_id טקסט עשיר

טיפול בכשלים:

מצב התנהגות
הספק מסרב לכרטיס בתוספת התוספת נשלחת שוב מיד בלעדיו; הפרוזה עדיין מגיעה בזמן אמת והכרטיסים מושלכים לכל חיי החיבור
הספק מסרב גם לפרוזה הזרם נסגר והתשובה מפורסמת שלמה
הזרם נשבר אחרי שחלק מהתשובה כבר נחת הזרם נסגר ושום דבר לא מפורסם שוב

ראו גשרי צ’אט למה המטען מפוצל כך.

הכרטיס נפתח כשאזכור מתקבל, לפני השיבוט ולפני האסימון הראשון; התור שבא אחר כך מאמץ את אותו כרטיס וזרם. הקריינות שאחרי זה מגיעה מ-SpaceProvisioningChanged על אפיק אירועי התחום — אותה התקדמות שבאנר ההקמה של אפליקציית שולחן העבודה קורא משורת המרחב.

מצב הקמה קריינות
האזכור התקבל Working on it…
שלב repo Cloning <repo>…
שלב prCheckout Checking out the pull request in <repo>…
שלב script Running the setup script for <repo>…
שלב agent Setting up <agent>…
provisioning, עדיין בלי שלב Preparing the workspace…
ready Starting the agent…
הסוכן חושב Thinking…
failed Workspace setup failed — details are in Control Center.
cancelled Workspace setup was stopped — details are in Control Center.

קריינות ההקמה דורשת את כל ארבעת התנאים:

תנאי
הספק מציג כרטיסים
הזרמה זמינה בחיבור הזה
במרחב יש בקשת צ’אט בדרך
שום כרטיס לא מדווח כבר על תור שם

הבקשה נשארת בדרך עד שתור מתחיל או שההקמה נכשלת. אירוע ready בלי כרטיס שכבר פתוח הוא מרחב חם ונשאר בשקט, והקמה שאיש בצ’אט לא ממתין לה — שיחה שנפתחה באפליקציה, או תקועה שהתחדשה באתחול — אינה מייצרת כרטיס כלל.

מוצרי צ’אט מקבלים http(s) בקישור ושום דבר אחר, ולכן קריאה לפעולה לא יכולה לנקוב ישירות בסכימה של אפליקציית שולחן העבודה.

מאפיין ערך
נתיב GET /open/workspaces/<workspaceId>/(spaces|tickets)/<id>
מקור נגזר מכתובת ה-URL הציבורית של השרת (ws → http, wss → https); loopback נשמר
תגובה עמוד HTML קטן עם no-store שמנווט אל control-center://workspaces/<workspaceId>/(spaces|tickets)/<id> ומציג כפתור ידני
אימות מזהים חייבים להתאים ל-[A-Za-z0-9_.-]{1,128}; כל דבר אחר הוא 404
גישה ציבורי ואינרטי — הוא לא קורא שום מסד נתונים, ולכן אינו אורקל קיום; האפליקציה שהוא פותח מבצעת את האישור
שדה מגבלה הערות
appName 1–35 תווים ברירת מחדל: Control Center · <workspace name>
botDisplayName 1–80 תווים ברירת מחדל: control-center
description ≤ 140 תווים
agentDescription ≤ 300 תווים מוצג מעל הודעה ישירה עם הבוט
command ≤ 32 תווים מנורמל: קווים נטויים מובילים מוסרים, אותיות מוקטנות, [^a-z0-9_-] מוסר, ריק נופל חזרה ל-cc
agentEnabled — האם חוויית ההודעה הישירה/סוכן חשופה

תיבה ריקה פירושה “השאר אותו כמו שהוא”, לא “נקה אותו”. המגבלות הן של Slack, שהן ההדוקות מבין המוצרים שמופו.

מאפיין ערך
אורך 6 תווים
אלפבית 23456789ABCDEFGHJKLMNPQRSTVWXYZ (בלי 0/O, 1/I, U)
תוקף 15 דקות
שימושים אחד
אחסון בזיכרון — הפעלה מחדש של השרת מבטלת קודים פתוחים
היקף סביבת עבודה אחת וספק אחד; הנפקת קוד חדש מבטלת את הקוד הקודם של החבר

המימוש מותאם בלי תלות ברישיות. קודים לא מוכרים, שפג תוקפם, של סביבת עבודה זרה ושל ספק שגוי נכשלים כולם באופן זהה בכוונה.

שיטה ערך תעבורה איך
קוד code החבר מימש /cc link <code>
אימייל email האימייל המאומת של הספק התאים למשתמש שהוא כבר חבר בסביבת העבודה

כולן בהיקף סביבת עבודה. השרת חסר מצב, ולכן כל קריאה נושאת את ה-workspace_id שלה ב-args; המשגר מסרב לפעולה בהיקף סביבת עבודה שמשמיטה אותו, לפני שמסד נתונים כלשהו נפתח. פרטי גישה נוסעים פנימה בלבד — קריאות סטטוס לעולם אינן מחזירות אותם.

פעולה סוג תפקיד מזערי מחלקות פעולה
chat.providers קריאה guest —
chat.status קריאה guest —
chat.connect שינוי admin secretAccess, networkEgress
chat.disconnect הרסנית admin secretAccess
chat.listUserLinks קריאה guest —
chat.beginUserLink שינוי member —
chat.unlinkUser שינוי member עבור עצמכם, admin עבור כל אחד אחר —
chat.createApp שינוי admin secretAccess, networkEgress
chat.setupLink קריאה admin —
chat.botProfile קריאה admin networkEgress
chat.updateBotProfile שינוי admin networkEgress

פעולות קריאה יורשות את רצפת ה-guest מהסוג שלהן; פעולות שינוי בלי רצפה מפורשת דורשות member.

chat.setupLink מרכיב כתובת URL ליצירת אפליקציית ספק מתוך פרופיל הבוט. הוא לא מצהיר על מחלקת פעולה כי הוא לא קורא שום סוד ולא מחייג לשום מקום — הדפדפן של המשתמש עצמו מבצע את הקריאה היחידה.

שאילתה ריאקטיבית אחת משלימה את המשטח:

שאילתה סוג פולטת
chat.watchUserLinks מעקב הרשימה, נדחפת מחדש בכל פעם שקישור נוצר או מוסר

היא קיימת כי הקישור קורה ב_צד האחר_: חבר מקליד את הקוד החד־פעמי אל תוך הבוט, ולכן שום תגובת לקוח לא יכולה לשאת את התשובה בחזרה. רשימת ההגדרות והדיאלוג הפתוח “קישור החשבון שלי” עוקבים אחריה, והדיאלוג עובר לאישור ברגע שהקישור של החבר עצמו נוחת.

chat.beginUserLink תמיד מנפיק קוד עבור הזהות של הקורא עצמו. chat.updateBotProfile מחזיר remaining_step רק כשנשאר שלב (בדרך כלל התקנה מחדש); היעדרו פירושו שהעריכה חיה.

chat.providers מחזיר רשומה אחת לכל ספק מוצע — המתאר ובנוסף, כשיש, סטטוס החיבור. כל מה שממשק ההגדרות מציג מגיע מהמטען הזה, ולכן הוספת ספק היא שינוי בצד השרת בלבד.

מה איפה
פרטי גישה של חיבור <dataDir>/<workspaceId>/chat_credentials/<provider>.json
הקמה מודרכת שנותרה באמצע <dataDir>/<workspaceId>/chat_credentials/<provider>_setup.json
קובצי Slack מדורות קודמים <dataDir>/<workspaceId>/slack_credentials.json ו-slack_app_setup.json, נקראים פעם אחת ומומרים במקום

פרטי הגישה יושבים לצד מסד הנתונים של סביבת העבודה, לא בתוכו: מחיקת סביבת עבודה מסירה איתה את אסימוני הצ’אט שלה, ו-workspace.export או גיבוי מעתיקים את workspace.db — כך שסביבת עבודה שנמסרת למישהו אחר אינה נושאת אסימוני בוט חיים. הכתיבות אטומיות (קובץ זמני ובנוסף שינוי שם) ומוקשחות להרשאת בעלים בלבד.

שתיהן יושבות במסד הנתונים של סביבת העבודה, שנושא בסיס v1 מכווץ — ולכן קריאה חוצת סביבות עבודה לא מתקמפלת.

chat_space_links — שיחה חיצונית אחת ↔ מרחב Control Center אחד:

עמודה הערות
id UUID v4
workspace_id היקף סביבת עבודה
provider ChatProvider.wire
external_team_id צוות Slack T…, ריק עבור ספקים בלי המושג
external_channel_id ערוץ Slack C…, קבוצה G…, הודעה ישירה D…
external_thread_id עוגן השרשור ב-Slack, או null כשכל השיחה היא העוגן (הודעות ישירות לבוט)
cc_space_id REFERENCES spaces(id) ON DELETE CASCADE
created_by_user_id, created_at, last_activity_at

ייחודי על (workspace_id, provider, external_channel_id, external_thread_id), עם אינדקס חלקי שאוכף את אותו דבר עבור מזהי שרשור null (SQLite מתייחס ל-NULLs כנפרדים).

chat_user_links — חבר חיצוני אחד ↔ משתמש Control Center אחד:

עמודה הערות
id UUID v4
workspace_id היקף סביבת עבודה
provider ChatProvider.wire
external_team_id, external_user_id חבר Slack U…
user_id משתמש Control Center (גלובלי, ולכן בלי מפתח זר)
method code או email
linked_at

ייחודי על (workspace_id, provider, external_team_id, external_user_id) ועל (workspace_id, provider, user_id) — ייחוס חד־ערכי בשני הכיוונים, בעוד שאדם אחד יכול להיות מקושר בכמה ספקים.

קישור הוא פענוח זהות, לא הענקת גישה: החברות נבדקת מחדש בכל אירוע נכנס, כך שקישור לחבר שהוסר מאז מפוענח כסירוב.

סוג המרחב ומטא־נתוני ההודעה

קטע בשם “סוג המרחב ומטא־נתוני ההודעה”

מרחב מגושר נוצר עם ה-SpaceKind של הספק (slack). הוא מתנהג כמו מרחב רגיל שפונה לאנשים — מתריע ומסווג כמו topic — ו-isExternalChat הוא הבדיקה שכל התנהגות (שיקוף, תג, שימור) עוברת דרכה.

כל הודעה שהגשר מכניס נושאת חתימה של metadata['chat']:

{
"chat": {
"provider": "slack",
"teamId": "T024BE7LD",
"channelId": "C0G9QF9GW",
"threadId": "1700000000.000100",
"messageId": "1700000000.000200",
"userId": "U024BE7LH",
"userLabel": "sam"
}
}

החתימה היא גם שמירת ההד: השיקוף היוצא מדלג על כל הודעה שנושאת אותה, וזה מה שמונע מהודעת צ’אט נכנסת להתפרסם מיד בחזרה.

התנהגות פירוט
מה מועבר החוצה הטקסט של הסוכן בלבד, בתוספת שורת הסטטוס של כרטיס המשימה. פרוזת החשיבה ופלט הכלים נשארים ב-Control Center
קצב ההזרמה מווסת לשטיפה אחת בשנייה בזמן שהתור רץ; הכרטיס והטקסט שהוא מסביר נוסעים באותה קריאה
הודעות של אנשים שהוקלדו ב-Control Center משוקפות אל השרשור המקושר, עם שם התצוגה של הכותב לפני כן
תורים ריקים מדווחים כהערה קצרה (ההרצה נכשלה, הופסקה או נעצרה במגבלת התורים שלה) ולא כשתיקה
הסרת כפילויות ממופתח לפי מזהה האירוע של הספק עצמו; 512 האחרונים נזכרים
סוכן החזית שיחה מגושרת חדשה מעירה את סוכן ה-CEO של סביבת העבודה, ואם אין — את הסוכן הראשון שלה, ואם אין — איש
שמות מרחבים #channel · <request> עבור שרשור בערוץ Slack, Slack DM · <request> עבור הודעה ישירה, כותרות מוגבלות ל-48 תווים
בידוד כשלים אירוע רע אחד לעולם לא מפיל את הגשר; החיבור ממשיך לשרת כל שיחה אחרת