إنتقل إلى المحتوى الرئيسي

واجهة دردشة العملاء (ابنِ واجهتك الخاصة)

ابنِ واجهة الدردشة الخاصة بك على منصّة 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أ. رمز الجلسة (كل الأوضاع)

  1. POST /session ← تحمل الاستجابة session_token (‏JWT صالح 30 يومًا) مع customer_id وchat_id.
  2. خزّنه وأرسله هكذا:
    • Authorization: Bearer <session_token> في كل نداء REST
    • ?access_token=<session_token> في عنوان الـ WebSocket
  3. للاستئناف لاحقًا، نادِ 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/tempmultipart/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 فقط