ربط الوكلاء بـ Webhooks
استقبال أحداث webhook وتشغيل مسارات عمل الوكيل استجابةً لها
ربط الوكلاء بـ Webhooks درس مجاني في AI Agents على CoddyKit. هذا هو الدرس 2 من أصل 4. يمكنك قراءة الدرس كاملاً أدناه مجاناً — ثم تمرن عليه مباشرة في المتصفح باستخدام محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7. هذا الدرس جزء من مسار التعلم في AI Agents، وتقدمك يتزامن عبر الويب وتطبيق CoddyKit. تتضمن دورة AI Agents 4 دروس في المجموع.
ما هو Webhook؟
إن Webhook هو استدعاء HTTP عكسي. عند وقوع حدث في خدمة خارجية، ترسل تلك الخدمة طلب POST إلى نقطة النهاية الخاصة بك مع بيانات الحدث. يعالج وكيلك الحمولة وينفّذ الإجراء المناسب.
تعتمد Webhooks على الدفع، أي تصل الأحداث عند وقوعها، بخلاف الاستطلاع الذي تتحقق فيه من البيانات بشكل متكرر.
نقطة نهاية Webhook في FastAPI
تسهّل FastAPI إنشاء مستقبِل Webhook. عرّف مسار POST، وحلّل نص JSON، ثم سلّمه إلى منطق الوكيل.
from fastapi import FastAPI, Request
from pydantic import BaseModel
app = FastAPI()
class WebhookPayload(BaseModel):
event: str
data: dict
@app.post('/webhook')
async def receive_webhook(payload: WebhookPayload):
print(f'Received event: {payload.event}')
print(f'Data: {payload.data}')
# Route to the right agent handler
if payload.event == 'email.received':
await handle_email_event(payload.data)
elif payload.event == 'file.uploaded':
await handle_file_event(payload.data)
return {'status': 'accepted'}
async def handle_email_event(data: dict):
print(f'Processing email from: {data.get("from")}')
async def handle_file_event(data: dict):
print(f'Processing file: {data.get("filename")}')التحقق من توقيع Webhook
تحقق دائمًا من أن طلبات Webhook واردة من المرسل المتوقع. توقّع معظم الخدمات حمولاتها باستخدام HMAC-SHA256 وباستخدام سر مشترك. ارفض الطلبات ذات التوقيعات غير الصالحة.
import hmac
import hashlib
from fastapi import FastAPI, Request, HTTPException
app = FastAPI()
WEBHOOK_SECRET = 'your-webhook-secret-here'
def verify_signature(payload_bytes: bytes, signature_header: str) -> bool:
expected = hmac.new(
WEBHOOK_SECRET.encode(),
payload_bytes,
hashlib.sha256
).hexdigest()
received = signature_header.replace('sha256=', '')
return hmac.compare_digest(expected, received)
@app.post('/webhook/verified')
async def verified_webhook(request: Request):
payload_bytes = await request.body()
signature = request.headers.get('X-Signature', '')
if not verify_signature(payload_bytes, signature):
raise HTTPException(status_code=401, detail='Invalid signature')
# Safe to process
import json
data = json.loads(payload_bytes)
return {'status': 'verified', 'event': data.get('event')}مفاتيح عدم تكرار التنفيذ
تعيد الخدمات الخارجية غالبًا إرسال عمليات تسليم Webhook الفاشلة. مفتاح عدم تكرار التنفيذ هو معرّف فريد يُرسل مع كل حدث. خزّن المفاتيح التي عولجت وتجاهل التكرارات.
from fastapi import FastAPI, Request, HTTPException
import redis
import json
app = FastAPI()
r = redis.Redis(host='localhost', port=6379, decode_responses=True)
@app.post('/webhook/idempotent')
async def idempotent_webhook(request: Request):
payload = await request.json()
# Extract idempotency key from header or payload
idempotency_key = request.headers.get('Idempotency-Key') or payload.get('event_id')
if not idempotency_key:
raise HTTPException(status_code=400, detail='Missing idempotency key')
redis_key = f'webhook:processed:{idempotency_key}'
# Check if already processed
if r.exists(redis_key):
print(f'Duplicate event {idempotency_key}, skipping')
return {'status': 'duplicate', 'idempotency_key': idempotency_key}
# Process event
# ... agent logic here ...
# Mark as processed (expire after 24h)
r.setex(redis_key, 86400, '1')
return {'status': 'processed', 'idempotency_key': idempotency_key}استراتيجية إزالة تكرار عمليات إعادة المحاولة
إضافةً إلى مفاتيح عدم تكرار التنفيذ، ضع في اعتبارك نوافذ إزالة التكرار. إذا تلقيت محتوى الحدث نفسه خلال فترة قصيرة، فمن المرجح أن تكون الرسالة إعادة محاولة. قارن تجزئات الأحداث لاكتشاف عمليات إعادة المحاولة وإسقاطها.
import hashlib
import json
from datetime import datetime
# In-memory store; use Redis in production
recent_hashes = {}
DEDUP_WINDOW_SECONDS = 300 # 5 minutes
def is_duplicate(payload: dict) -> bool:
# Hash the event content
content = json.dumps(payload, sort_keys=True)
event_hash = hashlib.md5(content.encode()).hexdigest()
now = datetime.utcnow().timestamp()
# Clean up old entries
expired = [h for h, ts in recent_hashes.items() if now - ts > DEDUP_WINDOW_SECONDS]
for h in expired:
del recent_hashes[h]
if event_hash in recent_hashes:
return True
recent_hashes[event_hash] = now
return False
# Test
payload = {'event': 'payment.completed', 'amount': 100}
print('First:', is_duplicate(payload)) # False
print('Second:', is_duplicate(payload)) # True (duplicate)تشغيل الوكيل بشكل غير متزامن
ينبغي لمعالجات Webhook أن تستجيب بسرعة (خلال أقل من 5 ثوانٍ) وأن تعالج منطق الوكيل في الخلفية. استخدم BackgroundTasks في FastAPI لتجنب انتهاء المهلة.
from fastapi import FastAPI, BackgroundTasks
import asyncio
app = FastAPI()
async def run_agent_job(event: str, data: dict):
print(f'Agent starting for event: {event}')
await asyncio.sleep(2) # Simulate LLM call
print(f'Agent finished for event: {event}')
@app.post('/webhook/async')
async def async_webhook(request_data: dict, background_tasks: BackgroundTasks):
event = request_data.get('event', 'unknown')
data = request_data.get('data', {})
# Respond immediately
background_tasks.add_task(run_agent_job, event, data)
return {'status': 'accepted', 'message': 'Processing in background'}تحليل الحمولات المعقدة
ترسل الخدمات المختلفة حمولات بأشكال مختلفة. اكتب دوال تحليل مخصصة لكل خدمة حتى يتلقى وكيلك دائمًا كائن حدث موحّدًا.
from dataclasses import dataclass
from typing import Optional
@dataclass
class NormalizedEvent:
event_type: str
source: str
resource_id: str
metadata: dict
def parse_github_webhook(payload: dict) -> NormalizedEvent:
return NormalizedEvent(
event_type='github.' + payload.get('action', 'unknown'),
source='github',
resource_id=str(payload.get('repository', {}).get('id', '')),
metadata={
'repo': payload.get('repository', {}).get('full_name'),
'sender': payload.get('sender', {}).get('login')
}
)
def parse_stripe_webhook(payload: dict) -> NormalizedEvent:
return NormalizedEvent(
event_type=payload.get('type', 'unknown'),
source='stripe',
resource_id=payload.get('id', ''),
metadata={'amount': payload.get('data', {}).get('object', {}).get('amount')}
)
# Usage
github_payload = {'action': 'opened', 'repository': {'id': 123, 'full_name': 'user/repo'}, 'sender': {'login': 'alice'}}
event = parse_github_webhook(github_payload)
print(event)أهمية رموز استجابة Webhook
أعِد رمز HTTP الصحيح. يخبر الرمز 2xx المرسلَ بأنه قُبل Webhook. ويعني الرمز 4xx وجود خطأ من جانب العميل (حمولة غير صحيحة). أما الرمز 5xx أو انتهاء المهلة فيؤدي إلى إعادة المرسل المحاولة.
- 200: قُبل وعولج
- 202: قُبل للمعالجة غير المتزامنة
- 400: طلب غير صالح (حقول مفقودة)
- 401: توقيع غير صالح
from fastapi import FastAPI, Request, HTTPException
from fastapi.responses import JSONResponse
app = FastAPI()
@app.post('/webhook/proper-responses')
async def proper_webhook(request: Request):
try:
payload = await request.json()
except Exception:
raise HTTPException(status_code=400, detail='Invalid JSON body')
required_fields = ['event', 'data']
for field in required_fields:
if field not in payload:
raise HTTPException(status_code=400, detail=f'Missing field: {field}')
event = payload['event']
known_events = ['email.received', 'file.uploaded', 'payment.completed']
if event not in known_events:
# Acknowledge unknown events gracefully - do not retry
return JSONResponse(status_code=200, content={'status': 'ignored', 'reason': 'unknown event'})
# Start background processing
return JSONResponse(status_code=202, content={'status': 'accepted'})اختبار Webhooks محليًا
استخدم ngrok لإتاحة خادمك المحلي عبر الإنترنت بغرض الاختبار. شغّل ngrok http 8000 للحصول على عنوان URL عام ينشئ نفقًا إلى تطبيق FastAPI المحلي.
# Start your FastAPI app
# uvicorn main:app --reload --port 8000
# In another terminal, start ngrok:
# ngrok http 8000
# You get: https://abc123.ngrok.io
# Now configure your webhook in Stripe/GitHub/etc. to:
# https://abc123.ngrok.io/webhook
# Test with curl:
import subprocess
def test_webhook_locally():
test_payload = '{"event": "email.received", "data": {"from": "test@example.com"}}'
# In real usage you would run this in terminal:
# curl -X POST http://localhost:8000/webhook \
# -H 'Content-Type: application/json' \
# -d '{"event": "email.received", "data": {"from": "test@example.com"}}'
print('Test payload:', test_payload)
print('Send to: http://localhost:8000/webhook')
test_webhook_locally()تسجيل أحداث Webhook
سجّل كل Webhook وارد مع الطابع الزمني والمصدر ونوع الحدث ونتيجة المعالجة. يُعد مسار التدقيق هذا ضروريًا لتصحيح مشكلات الأحداث الفائتة أو معالجة التكرارات.
import logging
import json
from datetime import datetime
import sys
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s %(levelname)s %(message)s',
stream=sys.stdout
)
logger = logging.getLogger('webhook')
def log_webhook_event(event_id: str, event_type: str, source: str, status: str, details: dict = None):
logger.info(json.dumps({
'timestamp': datetime.utcnow().isoformat(),
'event_id': event_id,
'event_type': event_type,
'source': source,
'status': status,
'details': details or {}
}))
# Usage in webhook handler
log_webhook_event(
event_id='evt_123',
event_type='email.received',
source='gmail',
status='processed',
details={'from': 'user@example.com', 'action_taken': 'reply_sent'}
)تحديد معدل Webhooks الواردة
احمِ نقطة نهاية Webhook من التعرّض للضغط الزائد باستخدام تحديد معدل الطلبات. تضيف مكتبة slowapi تحديد معدل الطلبات إلى FastAPI باستخدام قدر ضئيل من التعليمات البرمجية.
from fastapi import FastAPI, Request
from slowapi import Limiter, _rate_limit_exceeded_handler
from slowapi.util import get_remote_address
from slowapi.errors import RateLimitExceeded
limiter = Limiter(key_func=get_remote_address)
app = FastAPI()
app.state.limiter = limiter
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
@app.post('/webhook/limited')
@limiter.limit('100/minute')
async def rate_limited_webhook(request: Request):
payload = await request.json()
return {'status': 'accepted', 'event': payload.get('event')}اختبار المعرفة: Webhooks
اختبر مدى فهمك لأفضل ممارسات Webhooks الخاصة بالوكلاء.
Webhooks في بيئة الإنتاج
في بيئة الإنتاج، اجمع كل الأنماط: التحقق من التوقيع، ومفاتيح عدم تكرار التنفيذ، والمعالجة في الخلفية، والتسجيل المنظّم، وتحديد معدل الطلبات. انشر التطبيق خلف وكيل عكسي مثل nginx لإنهاء TLS وتوفير حماية إضافية.
الأسئلة الشائعة
هل درس «ربط الوكلاء بـ Webhooks» مجاني؟
نعم — نص درس «ربط الوكلاء بـ Webhooks» كامل متاح مجاناً هنا على الويب. لتمرينه بشكل تفاعلي (محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7) وفتح باقي دورة AI Agents، انتقل إلى CoddyKit PRO. تتضمن دورة AI Agents 4 دروس في المجموع.
ماذا ستتعلم في «ربط الوكلاء بـ Webhooks»؟
استقبال أحداث webhook وتشغيل مسارات عمل الوكيل استجابةً لها تتمرن على AI Agents مع أكواد عملية تشغلها مباشرة في المتصفح، ومدرس ذكاء اصطناعي متاح 24/7 يجيب على أسئلتك أثناء عملك.
هل أحتاج إلى خبرة سابقة لأبدأ AI Agents؟
لا تُشترط خبرة سابقة. AI Agents على CoddyKit منظم للمبتدئين حتى المتقدمين، لذا يمكنك البدء من هنا أو من البداية والتقدم بسرعتك الخاصة. هذا هو الدرس 2 من أصل 4.
كم من الوقت يستغرق درس «ربط الوكلاء بـ Webhooks»؟
معظم دروس CoddyKit تستغرق حوالي 5–10 دقائق. كل منها موجز وتفاعلي، لذا تحرز تقدماً مستمراً وتستأنف من حيث توقفت عبر الويب والتطبيق.
هل يمكنني كتابة وتشغيل أكواد في درس AI Agents هذا؟
نعم. كل درس في AI Agents يتضمن محرر أكواد مدمج، لذا تكتب وتشغل أكواداً حقيقية مباشرة في متصفحك وتحصل على تعليقات فورية من الذكاء الاصطناعي — بدون إعداد محلي.
جميع الدروس في هذه الدورة
- أنماط وكلاء المحفز والإجراء
- ربط الوكلاء بـ Webhooks
- الوكلاء المجدولون والقائمون على Cron
- بناء خط أنابيب لأتمتة تطبيقات متعددة