ابنِ بوابة API آمنة للذكاء الاصطناعي باستخدام Node.js

Share:

ابنِ بوابة صغيرة لـ API الذكاء الاصطناعي باستخدام Node.js تستقبل JSON مُتحقَّقاً منه، وتُسند معرّفات للطلبات، وتطبّق حداً محلياً لكل عميل، وتُمرّر الطلبات المعتمدة إلى خدمة نماذج على الخادم من دون كشف بيانات اعتماد المزوّد في كود المتصفح.

المتطلبات المسبقة

  • Node.js ١٨ أو إصدار أحدث. يستخدم هذا الدرس التقني إمكانات وقت التشغيل المدمجة في http وcrypto وfetch.
  • إلمام أساسي بـ JavaScript وJSON وطلبات HTTP ومتغيرات البيئة والطرفية.
  • خدمة نماذج مسموح لبوابتك باستدعائها عبر HTTP. لا تتضمن البوابة نموذجاً أو SDK لمزوّد نماذج.
  • عميل REST مثل curl أو Postman أو Bruno أو Insomnia أو إضافة VS Code REST Client.

ما الذي ستبنيه

يبني هذا الدرس التقني بوابة صغيرة عمداً لـ API الذكاء الاصطناعي باستخدام Node.js. يرسل العميل طلب JSON إلى POST /api/chat. تتحقق البوابة من أن الطلب يطابق البنية المتوقعة، وترفض الحمولات كبيرة الحجم، وتُسند معرّف طلب، وتطبّق تحديداً محلياً للمعدل، ثم تمرّر الطلب المتحقق منه إلى خدمة نماذج منفصلة عبر HTTP. بعد ذلك، تعيد البوابة استجابة خدمة النماذج إلى المستدعي.

ترتكز الفكرة المعمارية على فصل شائع للمسؤوليات: إذ تخدم الواجهة الخلفية للويب واجهات HTTP API، بينما تؤدي خدمة النماذج المهمة التنبؤية الأساسية. يصف السياق المتحقق منه مثالاً لمكدس يتضمن واجهة ويب أمامية، وموازن تحميل، وواجهة خلفية لـ REST API باستخدام Node.js، وقائمة مهام موزعة، وخدمة نماذج. تطبق هذه المقالة حدود بوابة Node.js فقط. ولا تدّعي أنها تفرض معمارية إنتاجية مكتملة، أو مورداً محدداً للنماذج، أو إطار عمل معيناً لخدمة النماذج.

إن إبقاء البوابة منفصلة عن خدمة النماذج يمنح التطبيق موضعاً واحداً لتطبيق قواعد الإدخال والضوابط التشغيلية قبل وصول العمل إلى نظام ذكاء اصطناعي. كما يعني أن تطبيقات المتصفح تستدعي خدمتك بدلاً من تلقي بيانات اعتماد حساسة للخدمة المصدرية. وتعتمد متطلبات المصادقة والتفويض والاحتفاظ بالبيانات ومراجعة السلامة والنشر الدقيقة على مؤسستك وحالة الاستخدام لديك، ولذلك لم تُمثَّل كإعدادات افتراضية عامة في هذا المثال.

يتجنب التنفيذ استدعاءات SDK الخاصة بالمزوّد التي لم يتم التحقق منها، وأسماء النماذج، وحدود الرموز المميزة، ومخططات استجابات النماذج. وبدلاً من ذلك، يعرّف عقد JSON صغيراً تملكه هذه التطبيق. وهذا مفيد عندما تكون خدمة النماذج خدمة داخلية، أو API استدلال تُشغَّل بصورة منفصلة، أو محولاً تحتفظ به في مكان آخر.

مسؤوليات البوابة وحدودها

ينبغي أن تكون مهمة البوابة محددة. في هذا الدرس التقني، تؤدي خمسة أمور: تستقبل طلبات HTTP، وتُحلل JSON محدود الحجم، وتتحقق من الحقول المطلوبة، وتسجل معرّف طلب معتماً، وتمرّر مدخلات آمنة إلى خدمة نماذج مصدرية. ولا تحاول تقرير ما إذا كانت الإجابة المولدة صحيحة. ولا تدرّب نموذجاً. ولا تضمّن المفاتيح في حزمة الواجهة الأمامية. كما أنها لا تفترض أن كل خدمة نماذج تستخدم بنية الطلب أو الاستجابة نفسها.

عقد الطلب العام لدينا بسيط عمداً:

{
  "messages": [
    { "role": "user", "content": "Explain an API gateway." }
  ]
}

تقبل البوابة ما يصل إلى ٢٠ رسالة، وتسمح فقط بالأدوار system وuser وassistant، وتحد كل رسالة بـ ١٢٬٠٠٠ حرف افتراضياً. هذه ضوابط على مستوى التطبيق وليست قياسات للرموز المميزة. فالأحرف ورموز النموذج المميزة ليست الوحدة نفسها، لذا لا ينبغي تقديم حد الأحرف على أنه حد دقيق للتكلفة أو الاستخدام.

تتلقى الخدمة المصدرية في هذا الدرس التقني غلافاً يحتوي على معرّف طلب والرسائل المتحقق منها. والاستجابة الناجحة المتوقعة منها هي JSON. لا تحوّل البوابة JSON هذا إلى تنسيق إكمال محايد للمزوّد، لأنه لم يتم التحقق من أي تنسيق لاستجابة مزوّد في السياق المتاح. إن امتلاك عقد داخلي صغير وموثّق أكثر أماناً من التخمين بشأن API لمزوّد ما.

الخطوة ١: أنشئ المشروع وملف البيئة

أنشئ دليلاً جديداً للمشروع. لا يستخدم هذا الإصدار أي تبعية خارجية، مما يجعل المثال سهل الفحص ويحافظ على سطح وقت تشغيل محدود. يكفي خادم HTTP المدمج في Node.js لعرض بوابة مركّز.

mkdir nodejs-ai-api-gateway
cd nodejs-ai-api-gateway
npm init -y
npm pkg set type=module
npm pkg set scripts.start="node src/server.js"
npm pkg set scripts.dev="node --watch src/server.js"
npm pkg set engines.node=">=18.0.0"
mkdir -p src

أنشئ ملف .gitignore قبل إنشاء الإعدادات المحلية. لا ترفع بيانات الاعتماد أو ملفات البيئة الخاصة بالنشر إلى نظام التحكم بالمصادر مطلقاً.

node_modules
.env
.env.local
.env.production
logs
coverage
npm-debug.log*
.DS_Store

بعد ذلك، أنشئ .env. يُعد MODEL_SERVICE_URL الإعداد المصدر الوحيد المطلوب. يجب أن يشير الرابط إلى خدمة يمكن لبيئة النشر الوصول إليها ومصرح لها باستخدامها. يستخدم المثال رابط loopback كقيمة للتطوير المحلي فقط.

PORT=3001
MODEL_SERVICE_URL=http://127.0.0.1:8080/generate
ALLOWED_ORIGIN=http://localhost:3000
MAX_BODY_BYTES=262144
MAX_MESSAGE_CHARS=12000
MAX_CONVERSATION_MESSAGES=20
REQUESTS_PER_MINUTE=30
UPSTREAM_TIMEOUT_MS=30000

لا تضع بيانات اعتماد الخدمة المصدرية في متغيرات بيئة مكشوفة للمتصفح. إذا كانت خدمة النماذج تتطلب بيانات اعتماد، فاحتفظ بها في بيئة النشر من جهة الخادم للبوابة وأرفقها على الخادم فقط. لا يتضمن هذا الدرس التقني ترويسة تفويض لأن اسمها وتنسيقها ودورة حياة بيانات الاعتماد الخاصة بها غير محددة في السياق المتحقق منه.

الخطوة ٢: تحقّق من الإعداد عند بدء التشغيل

أنشئ src/config.js. تصل متغيرات البيئة كسلاسل نصية، لذلك تحلل الوحدة القيم الرقمية صراحةً وتفشل مبكراً عندما يكون الإعداد المطلوب غير صالح. يُفضَّل التحقق عند بدء التشغيل على اكتشاف منفذ أو رابط مشوهين بعد وصول حركة المرور فقط.

import process from "node:process";

function readPositiveInteger(name, fallback, minimum, maximum) {
  const raw = process.env[name] ?? String(fallback);
  const value = Number.parseInt(raw, 10);

  if (!Number.isInteger(value) || value < minimum || value > maximum) {
    throw new Error(`${name} must be an integer from ${minimum} to ${maximum}.`);
  }

  return value;
}

function readUrl(name) {
  const raw = process.env[name];

  if (!raw) {
    throw new Error(`${name} is required.`);
  }

  try {
    return new URL(raw).toString();
  } catch {
    throw new Error(`${name} must be a valid URL.`);
  }
}

export const config = Object.freeze({
  port: readPositiveInteger("PORT", 3001, 1, 65535),
  modelServiceUrl: readUrl("MODEL_SERVICE_URL"),
  allowedOrigin: process.env.ALLOWED_ORIGIN ?? "http://localhost:3000",
  maxBodyBytes: readPositiveInteger("MAX_BODY_BYTES", 262144, 1024, 1048576),
  maxMessageChars: readPositiveInteger("MAX_MESSAGE_CHARS", 12000, 1, 100000),
  maxConversationMessages: readPositiveInteger(
    "MAX_CONVERSATION_MESSAGES",
    20,
    1,
    100
  ),
  requestsPerMinute: readPositiveInteger(
    "REQUESTS_PER_MINUTE",
    30,
    1,
    10000
  ),
  upstreamTimeoutMs: readPositiveInteger(
    "UPSTREAM_TIMEOUT_MS",
    30000,
    1000,
    120000
  )
});

للتطوير المحلي، حمّل قيم البيئة قبل بدء Node. أحد الخيارات المباشرة هو تصدير القيم في الصدفة لديك. ينبغي لمنصات النشر حقن متغيرات البيئة من جهة الخادم عبر آلية الإعدادات المحمية الخاصة بها. لا يعتمد الكود أعلاه عمداً على مكتبة إعدادات لم يتم التحقق منها.

الخطوة ٣: ابنِ بوابة Node.js

أنشئ src/server.js. الخادم مكتمل ويستخدم وحدات Node.js فقط. يقبل POST /api/chat وGET /health. ويتعامل مع طلبات preflight لمنشأ متصفح واحد مهيأ، لكن CORS ليست مصادقة. لا تزال البوابة العامة تحتاج إلى تصميم للمصادقة والتفويض مناسب لمستخدميها.

import crypto from "node:crypto";
import http from "node:http";
import { config } from "./config.js";

const rateWindows = new Map();

function sendJson(response, statusCode, body, requestId) {
  const payload = JSON.stringify(body);
  response.writeHead(statusCode, {
    "Content-Type": "application/json; charset=utf-8",
    "Content-Length": Buffer.byteLength(payload),
    "X-Request-Id": requestId,
    "Cache-Control": "no-store"
  });
  response.end(payload);
}

function getRequestId(request) {
  const supplied = request.headers["x-request-id"];
  if (typeof supplied === "string" && supplied.length > 0 && supplied.length <= 128) {
    return supplied;
  }
  return crypto.randomUUID();
}

function applyCors(request, response) {
  const origin = request.headers.origin;

  if (origin === config.allowedOrigin) {
    response.setHeader("Access-Control-Allow-Origin", origin);
    response.setHeader("Vary", "Origin");
    response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS");
    response.setHeader("Access-Control-Allow-Headers", "Content-Type, X-Request-Id");
    response.setHeader("Access-Control-Max-Age", "86400");
  }
}

function clientKey(request) {
  return request.socket.remoteAddress ?? "unknown";
}

function isRateLimited(request) {
  const key = clientKey(request);
  const now = Date.now();
  const windowStart = now - 60_000;
  const recent = (rateWindows.get(key) ?? []).filter((timestamp) => timestamp > windowStart);

  if (recent.length >= config.requestsPerMinute) {
    rateWindows.set(key, recent);
    return true;
  }

  recent.push(now);
  rateWindows.set(key, recent);
  return false;
}

function readJsonBody(request) {
  return new Promise((resolve, reject) => {
    let totalBytes = 0;
    const chunks = [];

    request.on("data", (chunk) => {
      totalBytes += chunk.length;
      if (totalBytes > config.maxBodyBytes) {
        reject(new Error("BODY_TOO_LARGE"));
        request.destroy();
        return;
      }
      chunks.push(chunk);
    });

    request.on("end", () => {
      try {
        const text = Buffer.concat(chunks).toString("utf8");
        resolve(JSON.parse(text));
      } catch {
        reject(new Error("INVALID_JSON"));
      }
    });

    request.on("error", reject);
  });
}

function validateChatRequest(value) {
  if (!value || typeof value !== "object" || Array.isArray(value)) {
    return "Request body must be a JSON object.";
  }

  if (!Array.isArray(value.messages) || value.messages.length === 0) {
    return "messages must be a non-empty array.";
  }

  if (value.messages.length > config.maxConversationMessages) {
    return `messages must contain at most ${config.maxConversationMessages} items.`;
  }

  const roles = new Set(["system", "user", "assistant"]);
  let systemCount = 0;

  for (const message of value.messages) {
    if (!message || typeof message !== "object" || Array.isArray(message)) {
      return "Each message must be an object.";
    }
    if (!roles.has(message.role)) {
      return "Each message role must be system, user, or assistant.";
    }
    if (typeof message.content !== "string" || message.content.trim().length === 0) {
      return "Each message content value must be a non-empty string.";
    }
    if (message.content.length > config.maxMessageChars) {
      return `Each message content value must be at most ${config.maxMessageChars} characters.`;
    }
    if (message.role === "system") systemCount += 1;
  }

  if (systemCount > 1) return "Only one system message is allowed.";
  if (value.messages[0].role === "assistant") {
    return "The first message cannot use the assistant role.";
  }

  return null;
}

async function forwardToModelService(input, requestId) {
  const timeout = AbortSignal.timeout(config.upstreamTimeoutMs);
  const upstreamResponse = await fetch(config.modelServiceUrl, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "X-Request-Id": requestId
    },
    body: JSON.stringify({ requestId, messages: input.messages }),
    signal: timeout
  });

  const contentType = upstreamResponse.headers.get("content-type") ?? "";
  const responseBody = contentType.includes("application/json")
    ? await upstreamResponse.json()
    : { message: await upstreamResponse.text() };

  return { status: upstreamResponse.status, body: responseBody };
}

const server = http.createServer(async (request, response) => {
  const requestId = getRequestId(request);
  applyCors(request, response);

  if (request.method === "OPTIONS") {
    response.writeHead(204, { "X-Request-Id": requestId });
    response.end();
    return;
  }

  if (request.method === "GET" && request.url === "/health") {
    sendJson(response, 200, { status: "ok", service: "nodejs-ai-api-gateway" }, requestId);
    return;
  }

  if (request.method !== "POST" || request.url !== "/api/chat") {
    sendJson(response, 404, { error: { code: "NOT_FOUND", message: "Route not found." } }, requestId);
    return;
  }

  if (isRateLimited(request)) {
    sendJson(response, 429, { error: { code: "RATE_LIMITED", message: "Try again shortly." } }, requestId);
    return;
  }

  if (!request.headers["content-type"]?.includes("application/json")) {
    sendJson(response, 415, { error: { code: "UNSUPPORTED_MEDIA_TYPE", message: "Use application/json." } }, requestId);
    return;
  }

  try {
    const input = await readJsonBody(request);
    const validationError = validateChatRequest(input);

    if (validationError) {
      sendJson(response, 400, { error: { code: "INVALID_REQUEST", message: validationError } }, requestId);
      return;
    }

    const upstream = await forwardToModelService(input, requestId);
    sendJson(response, upstream.status, { requestId, data: upstream.body }, requestId);
  } catch (error) {
    if (error.message === "BODY_TOO_LARGE") {
      sendJson(response, 413, { error: { code: "BODY_TOO_LARGE", message: "Request body exceeds the configured limit." } }, requestId);
      return;
    }

    if (error.message === "INVALID_JSON") {
      sendJson(response, 400, { error: { code: "INVALID_JSON", message: "Request body must contain valid JSON." } }, requestId);
      return;
    }

    console.error(JSON.stringify({ requestId, error: String(error) }));
    sendJson(response, 502, { error: { code: "MODEL_SERVICE_ERROR", message: "The model service could not be reached or did not complete the request." } }, requestId);
  }
});

server.listen(config.port, () => {
  console.log(JSON.stringify({ event: "listening", port: config.port }));
});

function shutdown(signal) {
  console.log(JSON.stringify({ event: "shutdown_started", signal }));
  server.close(() => process.exit(0));
  setTimeout(() => process.exit(1), 10_000).unref();
}

process.on("SIGINT", () => shutdown("SIGINT"));
process.on("SIGTERM", () => shutdown("SIGTERM"));

يستخدم محدد المعدل المحلي ذاكرة العملية وعنوان المقبس مفتاحاً له. وهذا مناسب فقط لعرض توضيحي بعملية واحدة. فعندما تعمل الخدمة خلف موازن تحميل أو على مثيلات متعددة، تكون لكل عملية ذاكرتها الخاصة وقد ترى عنوان عميل مختلفاً. استخدم آلية مشتركة ومصممة بعناية لتحديد المعدل، وإشارات هوية موثقة، قبل اعتبار الحدود أداة تحكم قابلة للإنفاذ على مستوى المؤسسة.

يسجل الخادم معرّف طلب معتماً وسلسلة خطأ فقط في مسار catch. ولا يسجل عمداً محتوى الرسائل. وهذا إعداد افتراضي عملي لأن المطالبات قد تحتوي على معلومات أعمال حساسة أو كود مصدري أو معلومات شخصية. وإذا احتجت إلى تسجيل المحتوى لعملية تقييم محددة، فقرر بصورة منفصلة ما الذي يمكن الاحتفاظ به، ومن يحق له الوصول إليه، وكيفية التعامل مع الحذف والاستجابة للحوادث.

الخطوة ٤: شغّل البوابة واختبرها

عيّن متغيرات البيئة في الصدفة لديك، ثم شغّل الخادم. يختلف بناء جملة الصدفة الدقيق بحسب نظام التشغيل. في صدفة شبيهة بـ Unix، يشغّل ما يلي التطبيق باستخدام الرابط المحلي في المثال.

export PORT=3001
export MODEL_SERVICE_URL=http://127.0.0.1:8080/generate
export ALLOWED_ORIGIN=http://localhost:3000
npm run dev

اختبر أولاً نقطة نهاية الحالة الصحية. فهي لا تستدعي خدمة النماذج.

curl --include http://localhost:3001/health

ثم أرسل طلب محادثة صالحاً. سيصل هذا الطلب إلى خدمة النماذج المهيأة، لذا لا يمكن أن ينجح إلا عندما تكون تلك الخدمة متاحة وتطبق العقد الذي يتوقعه تطبيقك.

curl --include --request POST http://localhost:3001/api/chat 
  --header "Content-Type: application/json" 
  --data '{
    "messages": [
      { "role": "system", "content": "Answer concisely." },
      { "role": "user", "content": "What is an API gateway?" }
    ]
  }'

اختبر أيضاً مسارات الفشل. أرسل JSON مشوهاً للتأكد من أن البوابة تعيد 400. وأرسل طلباً بدور غير صالح للتحقق من مخطط مستوى التطبيق. وأخيراً، أوقف خدمة النماذج مؤقتاً للتحقق من استجابة البوابة 502. اختبار الفشل ضروري لأن توفر الشبكة وسلوك الخدمة المصدرية غير مضمونين.

curl --include --request POST http://localhost:3001/api/chat 
  --header "Content-Type: application/json" 
  --data '{"messages":[{"role":"unknown","content":"Hello"}]}'

ما الذي ينبغي بناؤه لاحقاً

ابدأ بالمصادقة. تتحكم CORS في المتصفحات المتوافقة التي يمكنها قراءة الاستجابة؛ لكنها لا تحدد من المسموح له استخدام نقطة النهاية. أضف آلية مصادقة مناسبة لتطبيقك، ثم اربط الطلبات بمستخدم أو خدمة أو مؤسسة أو مستأجر قبل تطبيق حصص ذات معنى.

بعد ذلك، عرّف عقد خدمة النماذج صراحةً. وثّق نص الطلب المقبول، ونص الاستجابة المتوقع، وسلوك المهلة الزمنية، وحالات الخطأ، واستراتيجية الإصدار. إذا أدخلت قائمة انتظار للأعمال طويلة التشغيل، فاجعل دورة حياة الطلب صريحة بدلاً من التظاهر بأن كل مهمة ستكتمل ضمن استدعاء HTTP متزامن واحد. تتضمن معمارية المثال في السياق المتحقق منه قائمة مهام موزعة إلى جانب واجهة خلفية بـ Node.js وخدمة نماذج، وهو اتجاه مفيد لتقييمه لأحمال العمل التي لا تناسب تفاعل الطلب والاستجابة البسيط.

للتوسع، ضع البوابة خلف بنية تحتية يديرها فريقك ويراقبها. يصف السياق المتحقق منه موازن تحميل بوصفه جزءاً من مكدس إنتاجي نموذجي. إذا نشرت مثيلات متعددة من البوابة، فاستبدل المحدد الموجود في الذاكرة بنهج مشترك، وتأكد من تدفق معرّفات الطلبات عبر جميع الخدمات، واختبر كيفية التعامل مع معلومات عنوان العميل في إعدادات الوكيل لديك.

بالنسبة إلى عمليات النشر في دول مجلس التعاون الخليجي والشرق الأوسط، تجنب الافتراضات غير المدعومة بشأن الاستضافة المحلية أو المتطلبات القانونية أو البرامج الحكومية المسماة. وبدلاً من ذلك، اطلب من أصحاب المصلحة في الشؤون القانونية والأمن وحوكمة البيانات تقييم أماكن معالجة البيانات، والمعلومات التي تُرسل إلى خدمة النماذج، ومدة الاحتفاظ بالسجلات، والالتزامات التعاقدية أو التنظيمية الإقليمية المنطبقة على المؤسسة المحددة. تسهّل حدود البوابة وضع هذه الضوابط بصورة متسقة، لكنها لا تحقق الامتثال بذاتها.

وأخيراً، حافظ على تحديث التبعيات ومكونات وقت التشغيل بالتصحيحات. يتضمن السياق المتحقق منه إدخالاً في قاعدة بيانات الثغرات الوطنية، مما يعزز الحاجة العملية إلى مراقبة التنبيهات الأمنية ذات الصلة بالبرمجيات التي تشغّلها. راجع التنبيهات، واختبر الترقيات، وحافظ على عملية نشر قابلة للتكرار. إن بوابة مدمجة بعقد واضح أسهل في الاختبار والتدقيق والتطوير من استدعاءات خاصة بالمزوّد ومبعثرة عبر تطبيقات المتصفح والواجهة الخلفية.

Share:
الخطوة التالية: التنفيذ المباشر حلول مخصصة للشركات

هل تحتاج إلى حل ذكاء اصطناعي مخصص لأعمالك؟

حوّل أفكارك ومشاريعك إلى أنظمة إنتاج متكاملة — من النماذج اللغوية المخصصة ووكلاء الذكاء الاصطناعي المستقلين إلى أتمتة العمليات المعقدة. اطرح متطلبات مشروعك وتواصل مع نخبة المهندسين والشركات المعتمدة.

اطرح مشروعك واحصل على عروض مخصصة
تبحث عن حلول أتمتة ووكلاء جاهزين للتشغيل فوراً؟ استكشف حلول الأتمتة الجاهزة ←
حماية اتفاقية عدم الإفصاح (NDA)
عروض معتمدة خلال 24-48 ساعة
استشارة وتحديد نطاق مجاناً (0$ مقدماً)

هل كان هذا الشرح مفيداً؟

هل لديك سؤال حول هذا الشرح؟

مساعدنا الذكي قرأ هذا الشرح وهو جاهز للإجابة على استفساراتك المعمارية والبرمجية فوراً.

🤖
مرحباً بك! يمكنك سؤالي عن أي كود أو مفهوم تقني ورد في هذا الشرح.
مساعد الذكاء الاصطناعي متاح مجاناً للمهندسين والمشتركين.
تسجيل دخول مجاني لبدء المحادثة
🛡️ سوق حلول الإنتاج المؤسسية (0% عمولة رموز BYOK)

أنظمة وأكواد جاهزة للتشغيل مرتبطة بهذا الموضوع

تصفح سوق الحلول بالكامل →
أدوات المطورين • BYOK 🛡️ من $14/شهرياً

معماري أوامر المطورين والوكلاء (BYOK)

بيئة هندسية لبناء أوامر النظام (System Prompts)، قوالب LangGraph و CrewAI، مع دعم مفاتيح BYOK الخاصة بك.

مشروع مخصص • ضمان Escrow 🛡️ عرض سعر مخصص

بناء أنظمة ذكاء اصطناعي مخصصة (Escrow)

تعاقد مع مهندسي ذكاء اصطناعي معتمدين لبناء أو تخصيص أنظمة RAG ووكلاء بمفاتيح BYOK تحت حماية الضمان المالي.

Other • BYOK $499

AI SaaS Website Analyzer Tool

Instantly analyze any website URL and get detailed AI-powered insights, website quality assessments, SEO analysis, and actionable recommendations.

مرشد بوابة الذكاء الاصطناعي
نشط للخدمة
مرحباً بك في بوابة الذكاء الاصطناعي! أنا مرشدك هنا لمساعدتك في فهم خدمات المنصة، باقات التشغيل وخطط الضمان المالي. كيف يمكنني إرشادك اليوم؟