عقد خدمة الصوت
ويب هوك الموقَّع الذي تستدعيه خدمة الصوت لإنشاء المكالمات وإضافة أدوار النص وإغلاق المكالمات.
عقد فقط
هذا عقد. خدمة الصوت بـ Python (تحويل الكلام إلى نص، ونموذج اللغة، وتحويل النص إلى كلام عبر WebRTC وSIP) لم تُبنَ بعد. جانب Callwise من ويب هوك مبني ومختبر؛ ولا شيء يستدعيه في الإنتاج.
خدمة الصوت هي الكاتب الوحيد لسجلات المكالمات. تبلّغ التطبيق عن كل مكالمة بسلسلة أحداث. مصدر الحقيقة للحمولة هو مخطط zod المسمى voiceEventSchema في apps/web/lib/callwise/voice-events.ts.
نقطة النهاية
POST /api/voice/events
Content-Type: application/json
x-callwise-timestamp: 1760530212
x-callwise-signature: 5f0c2e…(64 hex characters)التوقيع
| الترويسة | القيمة |
|---|---|
x-callwise-timestamp | وقت Unix بالثواني عند توقيع الطلب |
x-callwise-signature | HMAC-SHA256 بالنظام الست عشري لـ `${timestamp}.${rawBody}` باستخدام السر المشترك CALLWISE_VOICE_SECRET |
وقّع البايتات التي ترسلها بالضبط. يعيد التطبيق حساب التوقيع على الجسم الخام قبل تحليله.
import { createHmac } from 'node:crypto';
const timestamp = Math.floor(Date.now() / 1000).toString();
const rawBody = JSON.stringify(event);
const signature = createHmac('sha256', process.env.CALLWISE_VOICE_SECRET!)
.update(`${timestamp}.${rawBody}`)
.digest('hex');import hmac, hashlib, json, os, time
timestamp = str(int(time.time()))
raw_body = json.dumps(event, separators=(",", ":"))
signature = hmac.new(
os.environ["CALLWISE_VOICE_SECRET"].encode(),
f"{timestamp}.{raw_body}".encode(),
hashlib.sha256,
).hexdigest()تُرفض الطلبات التي يبعد طابعها الزمني أكثر من 5 دقائق عن ساعة الخادم، وهو ما يوقف إعادة إرسال الطلبات القديمة.
الاستجابات
| الحالة | متى |
|---|---|
200 { "ok": true, "duplicate": false } | طُبِّق الحدث |
200 { "ok": true, "duplicate": true } | طُبِّق حدث بهذا id من قبل؛ ولم يتغير شيء |
400 | الجسم ليس JSON صالحًا، أو لا يطابق المخطط (مثلًا نوع حدث مجهول) |
401 | التوقيع مفقود أو خاطئ، أو الطابع الزمني خارج نافذة 5 دقائق |
تماثل المفعول
لكل حدث id خاص به. إرسال الحدث نفسه مرتين آمن: يُقر الثاني بـ duplicate: true ويُتجاهل. أعد المحاولة عند أخطاء الشبكة و5xx بـ نفس id.
شكل الحدث
لكل حدث الغلاف نفسه:
| الحقل | النوع | ملاحظات |
|---|---|---|
id | سلسلة، من 1 إلى 200 حرفًا | معرّف الحدث، فريد لكل حدث |
type | call.started | call.turn | call.ended | |
accountId | uuid | مساحة العمل (حساب الفريق) |
call | كائن | call.id (uuid) مطلوب. تختاره خدمة الصوت وتستخدم المعرّف نفسه لكل حدث من تلك المكالمة. |
turn | كائن | على call.turn فقط |
call.started
ينشئ مكالمة مباشرة. يُطابَق المتصل برقم الهاتف، أو يُنشأ. إذا تُرك agentId، يُختار الوكيل من الرقم الذي تم الاتصال به.
{
"id": "evt_01JA8S0K6Q2M3",
"type": "call.started",
"accountId": "3c1d6c1e-5c55-4f3e-9d0b-6b1f6f2a7a10",
"call": {
"id": "9b2f7d4e-1a7c-4e0f-8f5e-2d6c4b9a1e33",
"to": "+15552108842",
"callerPhone": "+15551237890",
"callerName": "Emily Johnson",
"direction": "inbound",
"startedAt": "2026-10-15T13:41:02Z",
"isTest": false,
"afterHours": false
}
}حقول call الاختيارية: numberId (uuid) أو to (E.164)، وagentId، وlocationId، وcallerPhone، وcallerName، وdirection (inbound أو outbound)، وstartedAt (ISO 8601)، وisTest، وafterHours.
call.turn
يضيف سطرًا واحدًا من النص. لا يحمل call إلا المعرّف. تُخزَّن أدوار النص حسب (معرّف المكالمة، seq)، لذا إعادة إرسال دور تستبدله.
{
"id": "evt_01JA8S0M1T7Z4",
"type": "call.turn",
"accountId": "3c1d6c1e-5c55-4f3e-9d0b-6b1f6f2a7a10",
"call": { "id": "9b2f7d4e-1a7c-4e0f-8f5e-2d6c4b9a1e33" },
"turn": {
"seq": 1,
"atMs": 4200,
"speaker": "caller",
"text": "Hi, I need a cleaning next week."
}
}حقل turn | النوع |
|---|---|
seq | عدد صحيح ≥ 0، الترتيب داخل المكالمة |
atMs | عدد صحيح ≥ 0، ميلي ثانية منذ بدء المكالمة |
speaker | agent | caller | staff | system |
text | سلسلة نصية |
call.ended
يغلق المكالمة. الدقائق المفوترة هي ceil(durationS / 60) وتُضاف إلى استخدام اليوم. ثم يحصّل التطبيق أرصدة للمكالمة: الدقائق المفوترة × معدل مكدس نماذج الوكيل + ملخص واحد (انظر نماذج الذكاء الاصطناعي والأرصدة)، تُؤخذ من رصيد الأرصدة المشترك. إذا تُرك summary، يُملأ ملخص من قالب.
عند call.started، تخبر الاستجابة خدمة الصوت كيف توجّه المكالمة: route: "agent"، أو route: "fallback" مع reason عندما يقول رصيد الأرصدة أو حد التزامن أو الحد الساعي لا. لا تُسقط المكالمات أبدًا.
{
"id": "evt_01JA8S2B9W0K8",
"type": "call.ended",
"accountId": "3c1d6c1e-5c55-4f3e-9d0b-6b1f6f2a7a10",
"call": {
"id": "9b2f7d4e-1a7c-4e0f-8f5e-2d6c4b9a1e33",
"endedAt": "2026-10-15T13:42:26Z",
"durationS": 84,
"intent": "booking",
"topic": "Dental cleaning",
"outcome": "booked",
"sentiment": "positive",
"confidence": 0.96,
"summary": "New caller booked a dental cleaning for Tuesday at 10:00.",
"recordingPath": "3c1d6c1e-5c55-4f3e-9d0b-6b1f6f2a7a10/9b2f7d4e-1a7c-4e0f-8f5e-2d6c4b9a1e33.mp3"
}
}حقل call | القيم |
|---|---|
endedAt | ISO 8601 |
durationS | ثوانٍ بعدد صحيح |
intent | booking reschedule cancel pricing insurance billing hours emergency general spam |
topic | نص حر |
outcome | booked rescheduled cancelled info transferred voicemail missed spam |
sentiment | positive neutral negative |
confidence | 0–1 |
summary | نص حر |
handoffReason | نص حر، عندما تكون النتيجة transferred |
recordingPath | <accountId>/<callId>.mp3 في الحاوية الخاصة cw-recordings |
كيف يُطبَّق
يتحقق المسار من التوقيع والمخطط، ثم يستدعي دالة Postgres المسماة cw_apply_voice_event(jsonb) بدور الخدمة. دور الخدمة وحده يستطيع تشغيل تلك الدالة، وليس للمستخدمين الذين سجّلوا الدخول صلاحية إدراج على المكالمات أو أدوار النص، لذا ويب هوك هو الطريقة الوحيدة لإنشاء سجلات المكالمات.
قبل الإطلاق
انظر مدخل خدمة الصوت ضمن الاتصالات لمعرفة ما يجب التحقق منه: زمن الاستجابة، والتعامل مع المقاطعة، وتدوير السر.