واجهة دردشة العملاء (ابنِ واجهتك الخاصة)
ابنِ واجهة الدردشة الخاصة بك على منصّة Orki — تصميمك ومكوّناتك، بينما يستمر عمل وكلاء الذكاء الاصطناعي والتسليم البشري وقاعدة المعرفة ولوحة المشغّلين دون تغيير. هذه هي نفس الواجهة البرمجية التي يعمل بها الودجت الرسمي — لا شيء هنا من الدرجة الثانية.
اختصارات: @orki/chat-core (JS/TS —
متصفح، React Native، Node) وصنف OrkiChatClient داخل
orki_webchat (Dart/Flutter) يغلّفان كل ما في هذه
الصفحة. تابع القراءة إن أردت نداء الواجهة مباشرة أو البناء لتقنية أخرى.
ملعب Postman: المجموعة ·
بيئة الإنتاج — استوردهما، عبّئ
tenant_id / integration_id، وشغّل Mint / resume session أولًا (سكربت الاختبار فيه
يلتقط session_token وcustomer_id وchat_id كمتغيرات لبقية الطلبات).
تعرض المجموعة المسار المجهول؛ في تكامل بوضع jwt أضف "identityToken": "…" إلى جسم ذلك
الطلب كما في §2ب أدناه.
1. البنية في فقرة واحدة
تتحدث واجهتك مع خدمة واحدة عبر مسار أساس واحد — واجهة الدردشة العامة:
https://app.orki.ai/services/public/ext/v1/tenants/{tenantId}/integrations/{integrationId}/...
كل شيء عدا إرسال الرسائل هو REST عادي (الجلسة، الملف الشخصي، السجل، رفع الوسائط، إيصالات القراءة). إرسال الرسائل واستقبال الردود يتم عبر SignalR WebSocket — لا توجد نقطة REST للإرسال عمدًا. قد تأتي الردود من وكيل ذكاء اصطناعي أو من موظف دعم بشري؛ يعرضهما عميلك بالطريقة نفسها.
-
https://app.orki.ai/services/public— واجهة الدردشة العامة (هذه الصفحة). جهة العميل، توثيق برمز الجلسة، وكل ما تحتاجه واجهة دردشة مخصصة. -
https://app.orki.ai/services/gateway— واجهة الإدارة/اللوحة (سطح المشغّلين المُوثَّق عبر Keycloak: الوكلاء، التكاملات، الحملات…). لا تنادِها أبدًا من واجهة موجهة للعملاء؛ ليست جزءًا من هذا التكامل.
تحتاج tenantId وintegrationId — أنشئ تكامل ويب من اللوحة (التكاملات ← الموقع)
وانسخهما من لوحة التثبيت.
2. التوثيق
طبقتان وقاعدة واحدة بسيطة: يحمل تطبيقك session_token قصير العمر ويرسله في كل مكان؛
أما طريقة الحصول عليه فتعتمد على وضع توثيق التكامل.
2أ. رمز الجلسة (كل الأوضاع)
-
POST /session← تحمل الاستجابةsession_token(JWT صالح 30 يومًا) معcustomer_idوchat_id. - خزّنه وأرسله هكذا:
-
Authorization: Bearer <session_token>في كل نداء REST -
?access_token=<session_token>في عنوان الـ WebSocket
-
- للاستئناف لاحقًا، نادِ
POST /sessionمجددًا مع الحامل — يعود نفس العميل ونفس المحادثة. عند401احذف الرمز المخزَّن وابدأ من جديد.
قاعدتان إلزاميتان:
- ترويسة
X-Public-Chat-Client: 1في كلPOST/PUT/DELETE(غيابها =403 {"error":"Missing X-Public-Chat-Client header"}). - إن أرسلت مكتبتك ترويسة
Originفيجب أن تطابق النطاق المسجَّل على التكامل (localhostمسموح دائمًا). العملاء الذين لا يرسلونها (تطبيقات أصلية، خوادم) غير متأثرين.
2ب. المستخدمون المعرَّفون — JWT موقَّع منك (المُوصى به)
للتكاملات في وضع jwt أو both
(راجع الزوّار المُوثَّقون)، يوقّع خادمك رمز هوية قصيرًا
وتستبدله الواجهة عند الإقلاع:
POST /session
Content-Type: application/json
X-Public-Chat-Client: 1
{ "identityToken": "<HS256 JWT: {sub, iat, exp≤24h, tenantId, integrationId, name?, email?}>" }
التوقيع الصالح يستبدل فحص الروبوتات كليًا — لا Turnstile، ويُربط الزائر بـ sub
الخاص بك (نفس المحادثة على كل جهاز، customer.identityVerified: true). أعد
POST /session برمز identityToken جديد في أي وقت قبل انتهاء صلاحيته. هذا هو الوضع
لتطبيقاتك خلف تسجيل الدخول، وهو الوحيد المتاح لواجهات الجوال الأصلية المخصصة.
أكواد أخطاء 401: jwt_required، jwt_invalid_signature، jwt_expired،
jwt_not_yet_valid، jwt_exp_too_far، jwt_tenant_mismatch، jwt_integration_mismatch،
jwt_malformed. في وضع both مع إيقاف فرض الهوية يعيد الرمز غير الصالح 200 بجلسة
مجهولة مع الترويسة X-Orki-Identity-Error: <code> — راقبها وجدّد رمزك.
2ج. الزوّار المجهولون — Turnstile (واجهات الويب فقط)
في وضع anonymous/both، يجب أن تجتاز الجلسة الجديدة فحص Cloudflare Turnstile:
اعرض ودجت Turnstile في صفحتك بمفتاح Orki
(اطلبه منا) وأرسل الناتج:
{ "turnstileToken": "<الرمز من نداء Turnstile>" }
403 = فشل التحدي/رمز قديم ← أعد العرض وحاول ثانية. الاستئناف بحامل مخزَّن لا يحتاج
Turnstile أبدًا. ولأن Turnstile للمتصفح فقط، الوضع المجهول غير متاح للواجهات الأصلية
المخصصة — استخدم JWT الهوية هناك.
حدود المعدل: الجلسات المجهولة 30/عنوان IP/ساعة؛ إقلاعات JWT المُوثَّقة 600/تكامل/ساعة
(كلاهما 429).
استجابة الجلسة
المستوى الأعلى snake_case (تاريخيًا)؛ كل ما عداه في الواجهة camelCase:
{
"customer_id": "665f0c…",
"chat_id": "665f0d…",
"unread_count": 0,
"session_token": "eyJhbGciOiJIUzI1NiIs…",
"customer": { "id": "…", "name": "ليلى", "email": null, "phone": null,
"language": null, "identityVerified": true },
"chat": { "id": "…", "status": "unopened", "handler": "…",
"handlerName": "مايا", "unreadCount": 0 }
}
الجلسة المجهولة الجديدة تنشئ عميلًا شبحيًا ومحادثة unopened؛ تتحول إلى open
ويصبح العميل حقيقيًا تلقائيًا مع معالجة أول رسالة. وإذا سجّل زائر مجهول دخوله لاحقًا
(أرسلت identityToken على نفس الجلسة) تُرقّى محادثته في مكانها مع الحفاظ على السجل.
3. مرجع النقاط
كل المسارات نسبية إلى …/tenants/{tenantId}/integrations/{integrationId} ما لم يُذكر غير ذلك.
🔓 = مجهول · 🔑 = يتطلب الحامل · ✉️ = يتطلب أيضًا X-Public-Chat-Client: 1.
الإقلاع والملف الشخصي
| النقطة | ملاحظات | |
|---|---|---|
| 🔓 | GET "" | إعدادات الودجت/العلامة: primaryColor، starterMessages، initialForm، aiProfileName، authMode، showPoweredBy — مفيدة حتى لواجهة مخصصة |
| 🔓✉️ | POST /session | إنشاء أو استئناف — راجع §2. حقول الجسم كلها اختيارية: identityToken، turnstileToken، chatName |
| 🔑 | GET /customer/me | {customer, chat} — إذا كان customer: null نادِ POST /session |
| 🔑✉️ | PUT /customer/me | {"name": "…", "email": "…", "phone": "…"} — الحقول المحذوفة لا تُمس. لنموذج ما قبل الدردشة (تجاوزه للمستخدمين المعرَّفين بـ JWT؛ مطالباتهم ملأت الملف مسبقًا) |
المحادثة
| النقطة | ملاحظات | |
|---|---|---|
| 🔑 | GET /chats/{chatId} | {id, tenantId, status, handler, createdAt, platformId} — استطلعها إن أردت عرض «تتحدث الآن مع إنسان» |
| 🔑 | GET /chats/{chatId}/messages?pageSize=50 | الأحدث أولًا. الاستجابة {data, hasMore, nextBefore}. الصفحات الأقدم: كرر مع &before={nextBefore} |
| 🔑✉️ | POST /chats/{chatId}/read | {"lastMessageId": "…"} — يعلّم تلك الرسالة وما قبلها كمقروءة |
الوسائط
| النقطة | ملاحظات | |
|---|---|---|
| 🔑✉️ | POST /chats/{chatId}/media/temp | multipart/form-data بحقل file، ملف واحد. الحد 25 MiB (413)، فحص فيروسات/نوع (400) ← {"ref", "contentType", "sizeBytes", "fileName"} |
| 🔓 | GET {base}/temp-media/{tenantId}/{ref} | معاينة ملف مُدرَج (المرجع غير القابل للتخمين هو التفويض). ليس تحت بادئة tenants |
| 🔑✉️ | DELETE /chats/{chatId}/media/temp/{ref} | إلغاء الإدراج. متساوي الأثر، 204 |
| 🔑✉️ | POST /chats/{chatId}/media/from-temp | {"refs": […]} ← {"ids": […]} — يرقّي الملفات؛ تدخل ids في mediaIds للرسالة |
| 🔑✉️ | POST /chats/{chatId}/media | رفع قديم بخطوة واحدة ← {"id"} (يستخدمه الودجت للرسائل الصوتية). فضّل temp←promote |
| 🔑 | GET /chats/{chatId}/message/{messageId}/media | بيانات المرفقات: [{id, mimeType, name, size, width, height, numOfPages}] |
| 🔑 | GET /chats/{chatId}/message/{messageId}/media/{mediaId} | البايتات (يدعم طلبات النطاق). للتحميل عبر <img src> أضف ?access_token={session_token} |
| 🔓 | GET /chats/{chatId}/handler/photo · GET /default-handler/photo | صورة الوكيل (204 إن لم توجد) |
إرسال رسالة بمرفقات — التسلسل الكامل:
1. POST …/media/temp (لكل ملف) ← refs
2. (اختياري) معاينة GET …/temp-media/{tenantId}/{ref}
3. POST …/media/from-temp {"refs":[…]} ← ids
4. نداء hub: CustomerMessage { content: { text: "…", mediaIds: ids } }
4. موزّع SignalR (إرسال + استقبال)
wss://app.orki.ai/services/public/ext/v1/chat/hub?tenantId={t}&integrationId={i}&access_token={session_token}
استخدم مكتبة عميل SignalR — @microsoft/signalr (JS / React Native)،
com.microsoft.signalr (أندرويد)، Microsoft.AspNetCore.SignalR.Client (.NET/MAUI)،
signalr_netcore (Flutter). اتصل بـ skipNegotiation: true ونقل WebSockets، وبعد نجاح
POST /session فقط — يقطع الخادم الاتصالات التي لا يقود رمزها إلى عميل موجود.
import * as signalR from "@microsoft/signalr";
const BASE = "https://app.orki.ai/services/public/ext/v1";
const connection = new signalR.HubConnectionBuilder()
.withUrl(`${BASE}/chat/hub?tenantId=${tenantId}&integrationId=${integrationId}`, {
accessTokenFactory: () => sessionToken,
skipNegotiation: true,
transport: signalR.HttpTransportType.WebSockets,
})
.withAutomaticReconnect()
.build();
connection.on("Message", ({ tempId, message }) => {
if (tempId) reconcileOptimisticBubble(tempId, message); // صدى رسالتك
else renderIncoming(message); // رد الوكيل أو الإنسان
});
connection.on("MessageEdited", ({ message }) => mergeMessage(message));
connection.on("TypingIndicator", ({ show, isCustomer }) => {
if (isCustomer === false) setAgentTyping(show);
});
connection.onreconnected(() => refetchLatestHistoryPage());
await connection.start();
await connection.invoke("CustomerMessage", {
chatId,
tempId: crypto.randomUUID(),
content: { text: "مرحبًا!" },
correlationId: crypto.randomUUID(),
});
ما تناديه:
| الطريقة | الحمولة |
|---|---|
CustomerMessage | {chatId, tempId, content: {text?, mediaIds?}, replyTo?, correlationId} |
Typing | {chatId, userId: customer_id, show: true|false} — بحد أقصى show:true واحدة كل 10 ثوانٍ |
ما تستقبله:
| الحدث | الحمولة |
|---|---|
Message | {tempId?, message} — صدى رسالتك (بـ tempId الخاص بك) وكل رد وكيل/إنسان؛ message.isCustomer يحدد الجهة |
MessageEdited | نفس الشكل — ادمج تحديثات status/content |
TypingIndicator | {chatId, userId, show, isCustomer} |
ملحق الإطارات الخام — قيادة الموزّع من Postman (عروض/تنقيح)
في Postman: New ← WebSocket، الصق العنوان أعلاه، اتصل، ثم أرسل الإطارات بالترتيب.
كل إطار SignalR يجب أن ينتهي بمحرف الفصل غير المرئي 0x1E (يظهر هنا كـ ␞ — لا تكتبه
حرفيًا؛ ألحق البايت برمجيًا أو انسخه من ملف يحتويه فعلًا).
{"protocol":"json","version":1}␞ ← المصافحة؛ يرد الخادم {}␞
{"type":1,"target":"CustomerMessage","arguments":[{"chatId":"<CHAT_ID>","tempId":"<uuid>","content":{"text":"مرحبا"},"correlationId":"<uuid>"}]}␞
{"type":1,"target":"Typing","arguments":[{"chatId":"<CHAT_ID>","userId":"<CUSTOMER_ID>","show":true}]}␞
{"type":6}␞ ← نبض؛ أرسله كل ~20 ثانية وإلا قطعك الخادم
قيم type: 1 نداء (بالاتجاهين)، 3 إتمام، 6 نبض، 7 إغلاق من الخادم (حقل error
يوضح السبب).
5. نموذج الرسالة (ما تعرضه)
{
"id": "665f…",
"chatId": "…",
"isCustomer": true, // true = الزائر؛ false = وكيل ذكاء اصطناعي أو موظف
"sender": "…",
"content": {
"text": "مرحبًا!",
"mediaIds": ["…"],
"location": [lon, lat],
"carousel": [ { "header", "footer", "headerImage", "buttonTitle", "buttonLink" } ],
"choices": [ { "id", "title" } ]
},
"replyTo": { "messageId", "content", "sender", "isCustomer" },
"timestamp": "2026-08-11T09:12:33.123Z",
"status": "stored", // stored ← sent ← delivered ← read | failed
"readBy": [ { "userId", "timestamp" } ]
}
قواعد عرض تصنع التجربة الصحيحة:
- رسائل الوسائط تحمل
mediaIdsفقط — اجلب البيانات ثم اعرض حسب بادئةmimeType(صورة/فيديو ضمنيًا، مشغّل صوت، بطاقة مستند). للرسائل الحقيقية فقط (معرّف 24 محرفًا). - الرسائل الصوتية التي ترسلها تُفرَّغ نصيًا في الخادم ويرد الذكاء الاصطناعي على النص — أبقِ فقاعة العميل تعرض مشغّل الصوت المحلي.
- التسليم لإنسان لا يحتاج معالجة خاصة: تستمر الردود كـ
MessageبـisCustomer: false. - المحادثات المحلولة تُفتح ضمنيًا — أرسل
CustomerMessageأخرى فحسب.
يفيد عند تصميم واجهة المرفقات: الصور وملفات PDF ومستندات أوفيس تدخل نموذج الذكاء الاصطناعي فعلًا؛ الرسائل الصوتية تُفرَّغ تلقائيًا؛ الفيديو يُخزَّن للموظفين ولا يفسّره الذكاء الاصطناعي.
6. قائمة التكامل
- خزّن
session_token؛ استأنف عبرPOST /sessionبالحامل عند كل إقلاع - المستخدمون المعرَّفون: أنشئ JWT في الخادم، استبدله عند الإقلاع، وجدّده قبل
exp؛ عالج أكوادjwt_*وترويسةX-Orki-Identity-Error -
X-Public-Chat-Client: 1في كل طلب مُعدِّل - أنشئ
tempId(UUID) لكل رسالة صادرة؛ طابقه مع صدىMessage -
withAutomaticReconnect()+ أعد جلب أحدث صفحة سجل عند إعادة الاتصال - خفّف
Typing؛ نادِPOST /readعند ظهور أحدث رسالة - حدّ الرفع 25 MiB في العميل؛ اعرض أخطاء
413و400 - واجهات الويب المجهولة: اعرض Turnstile للجلسات الجديدة؛ الواجهات الأصلية: JWT فقط