المهام وخطافات الويب
توليد الوسائط (الفيديو والصور والموسيقى) غير متزامن: تنشئ مهمة (job)، ثم تسترجع النتيجة عند جاهزيتها. هناك طريقتان لمعرفة اكتمال المهمة: الاستطلاع (polling) وخطاف الويب (push). فيما يلي شرح لبنية المهمة وكيفية استرجاع النتيجة بموثوقية.
دورة حياة المهمة
POST /media/generate → status: processing ──► done (result_url جاهز)
│ └─► failed (error، وتُعاد الأموال)
└── (اختياري) callback_url → خطاف ويب فوري عند done/failed- الإنشاء. أرسل
POST /media/generateمع النموذج والمعلمات، وسيُرجعidورابطًا للاستطلاع:
{ "id": "beefb531-…", "model": "image/nano-banana-2", "status": "processing",
"poll": "https://nordrouter.com/media/job/beefb531-…" }الانتظار. تنتقل المهمة من
processingإلىdone(أوfailed). يستغرق توليد الصور ثوانٍ، بينما قد يستغرق توليد الفيديو والموسيقى من عشرات الثواني إلى بضع دقائق.النتيجة. عند اكتمال المهمة، تكون قيمة
result_urlمملوءة (رابط إلىnordrouter.com)؛ يُحتفظ بالملف لفترة محدودة (retention_daysمنGET /media/models) — نزّله للاحتفاظ به.
تنسيق استجابة GET /media/job/:id (وكذلك جسم طلب خطاف الويب، انظر أدناه) موحّد:
{ "id": "beefb531-…", "status": "done",
"result_url": "https://nordrouter.com/media/file/beefb531-…",
"cost_usd": 0.052, "error": null }status— إحدى القيمprocessingأوdoneأوfailed.result_url— تكون قيمته مملوءة فقط عندdone، وإلا فهيnull.result_url_2— المسار الثاني في نماذج الموسيقى (Suno)، وإلا فهوnull.results— جميع ملفات المهمة:[{ "index": 1, "name": "Результат 1", "ext": "png", "url": "…" }](في مهام 3D: النموذج والمعاينة، وفي فصل المسارات: مسارات «الغناء» و«الموسيقى الآلية» وغيرها).cost_usd— التكلفة الفعلية (تُخصم حسب الاستخدام؛ لا تُحتسب تكلفة عمليات التوليد الفاشلة، ويُعاد المبلغ المحجوز).error— نص الخطأ عندfailed، وإلا فهوnull.
الطريقة 1. الاستطلاع (polling)
أبسط طريقة هي طلب GET /media/job/:id دوريًا إلى أن تصبح قيمة status هي done أو failed:
curl https://nordrouter.com/media/job/beefb531-…أجرِ الاستطلاع كل 2–3 ثوانٍ. يعمل الاستطلاع دائمًا ولا يتطلب عنوانًا عامًا، لذا فهو مناسب للبرامج النصية ودفاتر الملاحظات وCI.
الطريقة 2. Webhooks (الدفع)
لتجنّب الاستعلام المتكرر، أرسل callback_url إلى POST /media/generate، وسنرسل طلب POST إلى عنوانك عند اكتمال المهمة:
curl https://nordrouter.com/media/generate \
-H "Authorization: Bearer sk-nr-YOUR-KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "image/nano-banana-2",
"input": { "prompt": "…" },
"callback_url": "https://your-server.com/webhooks/nordrouter",
"callback_secret": "любая-ваша-строка"
}'إذا قُبل callback_url، فستتضمن الاستجابة عند إنشاء المهمة "webhook": "registered". يُتحقَّق من callback_url فورًا عند إنشاء المهمة: إذا لم يكن صالحًا (ليس https أو كان عنوانًا خاصًا/محليًا)، فسيعيد الطلب 400 مع السبب ({ "error": "callback_url отклонён: …", "code": "invalid_callback" })، ولن تُنشأ المهمة.
محتوى Webhook
عندما تصل المهمة إلى الحالة done أو failed، يُرسل إلى callback_url لديك طلب POST يتضمن المحتوى نفسه الذي يعيده GET /media/job/:id، بالإضافة إلى الحقل event:
{ "event": "media.completed", "id": "beefb531-…", "status": "done",
"result_url": "https://nordrouter.com/media/file/beefb531-…",
"cost_usd": 0.052, "error": null }event— إماmedia.completed(نجاح) أوmedia.failed(خطأ).
التحقق من التوقيع
إذا عيّنت callback_secret، فسيحتوي الترويسة X-NR-Signature على sha256=<hmac> — وهو HMAC-SHA256 لمحتوى جسم الطلب الخام، باستخدام سرك مفتاحًا. تحقّق منه للتأكد من أن الطلب صادر منّا وليس مزوّرًا:
import hmac, hashlib
def verify(raw_body: bytes, header: str, secret: str) -> bool:
expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, header) # مقارنة بزمن ثابتimport crypto from "node:crypto";
function verify(rawBody, header, secret) {
const expected = "sha256=" + crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(header));
}احسب HMAC من البايتات كما وصلت تمامًا، قبل تحليل JSON، وإلا فلن يتطابق التوقيع.
مستقبِل جاهز في 10 أسطر
خادم يعمل ويستقبل Webhook ويتحقق من التوقيع ويردّ برمز 2xx. السر هو نفسه الذي أرسلته في callback_secret.
Node.js / Express:
import express from "express";
import crypto from "node:crypto";
const SECRET = "любая-ваша-строка"; // = ваш callback_secret
const app = express();
app.post("/webhooks/nordrouter",
express.raw({ type: "application/json" }), // сырое тело — обязательно для подписи
(req, res) => {
const expected = "sha256=" + crypto.createHmac("sha256", SECRET).update(req.body).digest("hex");
const got = req.headers["x-nr-signature"] || "";
if (expected.length !== got.length || !crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(got)))
return res.sendStatus(401); // подпись не сошлась — не наш запрос
const job = JSON.parse(req.body);
if (job.event === "media.completed") console.log("готово:", job.result_url, "$" + job.cost_usd);
res.sendStatus(200); // ответьте 2xx, иначе будет повтор
});
app.listen(3000);Python / FastAPI:
import hmac, hashlib
from fastapi import FastAPI, Request, Response
SECRET = b"любая-ваша-строка" # = ваш callback_secret
app = FastAPI()
@app.post("/webhooks/nordrouter")
async def hook(request: Request):
raw = await request.body() # сырые байты — обязательно для подписи
expected = "sha256=" + hmac.new(SECRET, raw, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, request.headers.get("X-NR-Signature", "")):
return Response(status_code=401) # подпись не сошлась — не наш запрос
job = await request.json()
if job["event"] == "media.completed":
print("готово:", job["result_url"], "$", job["cost_usd"])
return Response(status_code=200) # ответьте 2xx, иначе будет повторالتحقق محليًا
يجب أن يكون callback_url عنوان https عامًا. للتطوير على حاسوبك المحمول، أنشئ نفقًا، مثل ngrok http 3000، وأرسل عنوان https الذي يوفّره بوصفه callback_url.
التسليم والموثوقية
- حتى 4 محاولات مع فواصل زمنية متزايدة (≈3 → 6 → 9 ثوانٍ)، ومهلة كل محاولة 15 ثانية. أعدّ
2xx، وإلا فستُعاد المحاولة. - لا تُتبع عمليات إعادة التوجيه — أجب مباشرةً على
callback_url. - التسليم بأفضل جهد: إذا فشلت المحاولات الأربع كلها (مثلًا، كان خادمك متوقفًا)، فلن يُعاد إرسال Webhook لاحقًا. لذلك:
Webhooks لا تحل محل الاستعلام
Webhook وسيلة لتسريع الإشعار، وليس ضمانًا. أبقِ GET /media/job/:id مسارًا احتياطيًا: إذا لم يصل الإشعار الفوري خلال مدة معقولة، فتحقّق من الحالة بنفسك. تقع مسؤولية ضمان عدم تكرار المعالجة على جانبك: قد تصلك المهمة نفسها عبر Webhook وتلاحظها من خلال الاستعلام، لذا اعتمد على id.
متطلبات callback_url
- يجب أن يبدأ بـ
https://فقط. - يجب أن يكون عنوانًا عامًا فقط. تُرفَض العناوين المحلية والخاصة (
localhostو*.localو10.xو172.16–31.xو192.168.xو127.xو169.254.xو100.64–127.xوعناوين IPv6 loopback/ULA/link-local) للحماية من SSRF. ويُعاد التحقق من العنوان قبل كل إرسال أيضًا (للحماية من DNS rebinding).
ماذا تختار
| الاستعلام | Webhook | |
|---|---|---|
عنوان https عام | غير مطلوب | مطلوب |
| تأخير الاستلام | حتى فترة الاستعلام | شبه فوري |
| الضمان | تتحكم به بنفسك | أفضل جهد + الاستعلام كمسار احتياطي |
| الأنسب لـ | السكربتات وCI والحاسوب المحمول | الخوادم والأنظمة الخلفية للإنتاج |
التوصية: في الأنظمة الخلفية، استخدم Webhook مع الاستعلام كإجراء احتياطي؛ وفي السكربت أو على الحاسوب المحمول، استخدم الاستعلام فقط.