Cloudflare Workers چیست؟ آموزش ساخت API هوش مصنوعی Serverless با درواره
در این آموزش یاد میگیرید Cloudflare Workers و معماری Serverless چیست و چگونه با TypeScript یک API هوش مصنوعی امن، Streaming و قابل استقرار بسازید و آن را به API درواره متصل کنید.
برای ساخت یک API هوش مصنوعی همیشه به خرید سرور، نصب سیستمعامل، راهاندازی Nginx، مدیریت Process و پیکربندی Docker نیاز ندارید. در بسیاری از پروژههای سبک و متوسط میتوانید منطق Backend را روی یک زیرساخت Serverless اجرا کنید.
Cloudflare Workers یکی از پلتفرمهای محبوب برای اجرای کد JavaScript و TypeScript در شبکه Edge است. در این روش، شما کد API را مینویسید و پلتفرم اجرای آن، مقیاسپذیری، دریافت درخواست HTTP و استقرار را مدیریت میکند.
در این مقاله یک پروژه واقعی میسازیم که قابلیتهای زیر را دارد:
- دریافت پیام از Client
- اعتبارسنجی ورودی
- محافظت از API با Token
- نگهداری کلید درواره در Secret
- اتصال به API هوش مصنوعی درواره
- دریافت پاسخ کامل
- دریافت پاسخ Streaming
- مدیریت CORS
- ثبت Request ID
- مدیریت خطا و Timeout
- تست محلی با Wrangler
- استقرار روی
workers.dev - اتصال دامنه اختصاصی
- آمادهسازی برای استفاده عملی
Cloudflare Workers چیست؟
Cloudflare Workers یک محیط اجرای Serverless است که به توسعهدهندگان اجازه میدهد کد Backend را بدون مدیریت مستقیم سرور اجرا کنند.
کد Worker میتواند درخواست HTTP دریافت کند، آن را پردازش کند، با یک API خارجی ارتباط برقرار کند و پاسخ را به کاربر برگرداند.
معماری ساده آن به شکل زیر است:
User
|
v
Cloudflare Worker
|
v
External API
در پروژه این مقاله، API خارجی همان API درواره است:
کاربر یا اپلیکیشن
|
v
Cloudflare Worker
|
v
https://api.darvareh.ir/v1
|
v
مدل هوش مصنوعی
Worker در این معماری نقش Backend واسط را دارد. کلید API درواره داخل Worker نگهداری میشود و در اختیار مرورگر یا کاربر نهایی قرار نمیگیرد.
Serverless چیست؟
Serverless به این معنی نیست که هیچ سروری وجود ندارد. کد شما همچنان روی سرور اجرا میشود، اما مدیریت مستقیم سرور بر عهده ارائهدهنده زیرساخت است.
در یک معماری سنتی باید مواردی مانند اینها را مدیریت کنید:
- تهیه VPS
- نصب Linux
- نصب Runtime
- مدیریت Process
- تنظیم Reverse Proxy
- بهروزرسانی سیستمعامل
- مدیریت مقیاسپذیری
- بررسی مصرف CPU و RAM
- راهاندازی مجدد برنامه بعد از Crash
در معماری Serverless، تمرکز اصلی شما روی کد و تنظیمات برنامه است.
مقایسه کلی:
| ویژگی | سرور سنتی | Serverless |
|---|---|---|
| مدیریت سیستمعامل | بر عهده شما | بر عهده پلتفرم |
| نصب Runtime | لازم است | آماده است |
| مقیاسپذیری | نیازمند تنظیم | تا حد زیادی مدیریتشده |
| پرداخت | معمولاً براساس زمان سرور | معمولاً براساس مصرف |
| کنترل زیرساخت | زیاد | محدودتر |
| اجرای پردازش طولانی | مناسبتر | وابسته به محدودیت پلتفرم |
| API سبک و سریع | مناسب | بسیار مناسب |
| شروع پروژه | زمانبرتر | سریعتر |
Edge Computing چیست؟
در Cloudflare Workers کد میتواند در زیرساخت Edge اجرا شود. منظور از Edge، نقاطی از شبکه است که نسبت به یک سرور مرکزی به کاربران نزدیکتر هستند.
این معماری برای عملیات زیر مفید است:
- اعتبارسنجی درخواست
- مسیریابی API
- افزودن یا حذف Header
- بررسی Token
- Cache کردن پاسخهای مناسب
- تبدیل فرمت داده
- اتصال به سرویسهای خارجی
- ساخت API Gateway سبک
- اجرای منطق کوتاه و Stateless
نزدیک بودن محل اجرای Worker به کاربر، فقط یکی از عوامل Latency است. اگر Worker به یک API خارجی متصل شود، زمان پاسخ آن API و فاصله شبکه با سرویس بالادستی نیز روی زمان نهایی اثر میگذارد.
Cloudflare Workers برای چه پروژههایی مناسب است؟
Cloudflare Workers میتواند برای این کاربردها مناسب باشد:
- Backend سبک وبسایت
- API واسط برای مدل هوش مصنوعی
- فرم تماس و ثبت اطلاعات
- Webhook Receiver
- API Gateway کوچک
- پردازش و تغییر Header
- تولید پاسخهای JSON
- اتصال چند سرویس به یکدیگر
- Streaming پاسخ
- Redirect و URL Rewrite
- پردازش درخواست در Edge
برای عملیات زیر باید محدودیتهای پلتفرم را دقیقتر بررسی کنید:
- پردازش CPU بسیار سنگین
- اجرای طولانی در پسزمینه
- پردازش ویدئو روی Runtime
- نگهداری فایلهای بزرگ در حافظه
- برنامههای وابسته به کتابخانههای سیستمی خاص
- اتصالهای پیچیده و طولانیمدت
- پردازشهای نیازمند GPU
محدودیتها و امکانات پلنها ممکن است تغییر کنند. پیش از طراحی نسخه Production، صفحه محدودیتهای Cloudflare Workers را بررسی کنید.
چرا Worker را مستقیماً به API درواره متصل کنیم؟
فرض کنید یک صفحه وب دارید که از مدل هوش مصنوعی استفاده میکند. قرار دادن کلید API داخل JavaScript مرورگر اشتباه است:
const apiKey = "YOUR_DARVAREH_API_KEY";
هر کاربر میتواند Source، درخواستهای Network یا فایل JavaScript را ببیند و کلید را استخراج کند.
معماری درستتر:
Frontend
|
| کلید داخلی برنامه
v
Cloudflare Worker
|
| کلید محرمانه درواره
v
Darvareh API
در این معماری:
- کلید درواره در مرورگر قرار نمیگیرد.
- مدل از سمت Worker انتخاب میشود.
- طول ورودی محدود میشود.
- تعداد توکن خروجی کنترل میشود.
- پاسخ خطا قابل مدیریت است.
- دسترسی کاربران قابل کنترل است.
- امکان ثبت لاگ و Request ID وجود دارد.
پیشنیازهای آموزش
برای اجرای پروژه به موارد زیر نیاز دارید:
- حساب Cloudflare
- Node.js
- npm
- کلید API درواره
- شناسه یک مدل فعال در درواره
- آشنایی مقدماتی با JavaScript یا TypeScript
- Terminal، PowerShell یا Command Prompt
نسخه Node.js را بررسی کنید:
node --version
نسخه npm:
npm --version
ساخت پروژه Cloudflare Worker
دستور زیر را اجرا کنید:
npm create cloudflare@latest -- darvareh-ai-worker
هنگام ساخت پروژه گزینههای مشابه زیر را انتخاب کنید:
What would you like to start with?
Hello World example
Which template would you like to use?
Worker only
Which language do you want to use?
TypeScript
Do you want to use git?
Yes
Do you want to deploy?
No
وارد پوشه پروژه شوید:
cd darvareh-ai-worker
طبق راهنمای رسمی، ابزار ایجاد پروژه فایلهای موردنیاز Worker و Wrangler را میسازد. برای اجرای محلی نیز میتوان از wrangler dev استفاده کرد. راهنمای شروع Cloudflare Workers
ساختار پروژه
ساختار اولیه باید تقریباً مشابه زیر باشد:
darvareh-ai-worker/
├── src/
│ └── index.ts
├── test/
├── package.json
├── package-lock.json
├── tsconfig.json
├── worker-configuration.d.ts
└── wrangler.jsonc
فایلهای اصلی ما:
src/index.ts: منطق Workerwrangler.jsonc: تنظیمات Worker.dev.vars: Secretهای محیط توسعه.gitignore: فایلهای خارجشده از Git
تنظیم فایل wrangler.jsonc
محتوای فایل wrangler.jsonc را به شکل زیر تنظیم کنید:
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "darvareh-ai-api",
"main": "src/index.ts",
"compatibility_date": "2026-08-01",
"vars": {
"DARVAREH_MODEL_ID": "YOUR_MODEL_ID",
"ALLOWED_ORIGIN": "http://localhost:3000"
},
"secrets": {
"required": [
"DARVAREH_API_KEY",
"APP_TOKEN"
]
}
}
متغیر DARVAREH_MODEL_ID
شناسه مدلی است که Worker اجازه استفاده از آن را دارد:
"DARVAREH_MODEL_ID": "YOUR_MODEL_ID"
شناسه مدل مورد نظر خود را از صفحه مدلها و قیمتهای درواره بردارید.
بهتر است در نسخه اولیه، کاربر نتواند Model ID دلخواه را در Body درخواست ارسال کند. اگر مدل سمت سرور انتخاب شود، کنترل هزینه و رفتار برنامه سادهتر خواهد بود.
متغیر ALLOWED_ORIGIN
دامنه Frontend مجاز را مشخص میکند:
"ALLOWED_ORIGIN": "https://app.example.com"
برای توسعه محلی میتوانید از آدرس زیر استفاده کنید:
"ALLOWED_ORIGIN": "http://localhost:3000"
بخش secrets
این بخش نام Secretهای الزامی را تعریف میکند:
"secrets": {
"required": [
"DARVAREH_API_KEY",
"APP_TOKEN"
]
}
مقدار واقعی Secretها نباید داخل wrangler.jsonc قرار بگیرد.
ساخت Secretهای محیط توسعه
در Root پروژه فایلی با نام .dev.vars بسازید:
DARVAREH_API_KEY="YOUR_DARVAREH_API_KEY"
APP_TOKEN="یک-توکن-طولانی-و-تصادفی"
برای تولید Token تصادفی در Linux یا macOS:
openssl rand -hex 32
نمونه ساختار خروجی:
4c2f50bd73428183fb30c19f8ed00c06b6825317a79f...
این مقدار را بهعنوان APP_TOKEN قرار دهید.
فایل .gitignore باید شامل این موارد باشد:
node_modules/
.dev.vars
.dev.vars.*
.env
.env.*
.wrangler/
طبق مستندات Cloudflare، برای اطلاعات محرمانه باید از Secret استفاده شود و فایلهایی مانند .dev.vars یا .env نباید داخل Git قرار بگیرند. مستندات Secret در Cloudflare Workers
طراحی API پروژه
دو Endpoint اصلی میسازیم:
POST /v1/chat
POST /v1/chat/stream
مسیر اول پاسخ کامل JSON را برمیگرداند.
مسیر دوم پاسخ مدل را بهصورت Streaming منتقل میکند.
همچنین دو مسیر کمکی داریم:
GET /
GET /health
ساخت Worker با TypeScript
محتوای فایل src/index.ts را با کد زیر جایگزین کنید:
interface Env {
DARVAREH_API_KEY: string;
APP_TOKEN: string;
DARVAREH_MODEL_ID: string;
ALLOWED_ORIGIN: string;
}
type ClientRole = "user" | "assistant";
interface ClientMessage {
role: ClientRole;
content: string;
}
interface ChatBody {
messages: ClientMessage[];
temperature?: number;
max_tokens?: number;
}
const DARVAREH_CHAT_URL =
"https://api.darvareh.ir/v1/chat/completions";
const MAX_BODY_BYTES = 64 * 1024;
const MAX_MESSAGES = 30;
const MAX_MESSAGE_LENGTH = 10_000;
const MAX_TOTAL_CONTENT_LENGTH = 20_000;
const MAX_OUTPUT_TOKENS = 2_000;
function createCorsHeaders(
request: Request,
env: Env
): Headers {
const headers = new Headers();
const origin = request.headers.get("Origin");
if (origin && origin === env.ALLOWED_ORIGIN) {
headers.set("Access-Control-Allow-Origin", origin);
headers.set("Vary", "Origin");
}
headers.set(
"Access-Control-Allow-Methods",
"GET, POST, OPTIONS"
);
headers.set(
"Access-Control-Allow-Headers",
"Content-Type, Authorization"
);
headers.set(
"Access-Control-Max-Age",
"86400"
);
return headers;
}
function jsonResponse(
request: Request,
env: Env,
data: unknown,
status = 200,
requestId?: string
): Response {
const headers = createCorsHeaders(request, env);
headers.set(
"Content-Type",
"application/json; charset=utf-8"
);
headers.set(
"Cache-Control",
"no-store"
);
if (requestId) {
headers.set("X-Request-ID", requestId);
}
return new Response(
JSON.stringify(data),
{
status,
headers,
}
);
}
function isOriginAllowed(
request: Request,
env: Env
): boolean {
const origin = request.headers.get("Origin");
if (!origin) {
return true;
}
return origin === env.ALLOWED_ORIGIN;
}
function isAuthorized(
request: Request,
env: Env
): boolean {
const authorization =
request.headers.get("Authorization");
return authorization === `Bearer ${env.APP_TOKEN}`;
}
function validateChatBody(
value: unknown
):
| { ok: true; body: ChatBody }
| { ok: false; message: string } {
if (
typeof value !== "object" ||
value === null
) {
return {
ok: false,
message: "بدنه درخواست معتبر نیست.",
};
}
const body = value as Partial<ChatBody>;
if (
!Array.isArray(body.messages) ||
body.messages.length === 0 ||
body.messages.length > MAX_MESSAGES
) {
return {
ok: false,
message:
`تعداد پیامها باید بین ۱ تا ${MAX_MESSAGES} باشد.`,
};
}
let totalLength = 0;
for (const message of body.messages) {
if (
typeof message !== "object" ||
message === null
) {
return {
ok: false,
message: "ساختار یکی از پیامها معتبر نیست.",
};
}
if (
message.role !== "user" &&
message.role !== "assistant"
) {
return {
ok: false,
message:
"نقش پیام فقط میتواند user یا assistant باشد.",
};
}
if (
typeof message.content !== "string" ||
message.content.trim().length === 0
) {
return {
ok: false,
message: "محتوای پیام نمیتواند خالی باشد.",
};
}
if (
message.content.length >
MAX_MESSAGE_LENGTH
) {
return {
ok: false,
message:
"طول یکی از پیامها بیشتر از حد مجاز است.",
};
}
totalLength += message.content.length;
}
if (
totalLength >
MAX_TOTAL_CONTENT_LENGTH
) {
return {
ok: false,
message:
"مجموع طول پیامها بیشتر از حد مجاز است.",
};
}
const temperature =
body.temperature ?? 0.3;
if (
typeof temperature !== "number" ||
!Number.isFinite(temperature) ||
temperature < 0 ||
temperature > 2
) {
return {
ok: false,
message:
"مقدار temperature باید بین ۰ و ۲ باشد.",
};
}
const maxTokens =
body.max_tokens ?? 800;
if (
!Number.isInteger(maxTokens) ||
maxTokens < 1 ||
maxTokens > MAX_OUTPUT_TOKENS
) {
return {
ok: false,
message:
`مقدار max_tokens باید بین ۱ و ${MAX_OUTPUT_TOKENS} باشد.`,
};
}
return {
ok: true,
body: {
messages: body.messages,
temperature,
max_tokens: maxTokens,
},
};
}
async function parseRequestBody(
request: Request
): Promise<
| { ok: true; value: unknown }
| { ok: false; message: string }
> {
const contentType =
request.headers.get("Content-Type") ?? "";
if (
!contentType
.toLowerCase()
.includes("application/json")
) {
return {
ok: false,
message:
"Content-Type باید application/json باشد.",
};
}
const declaredLength =
request.headers.get("Content-Length");
if (
declaredLength &&
Number(declaredLength) > MAX_BODY_BYTES
) {
return {
ok: false,
message: "حجم درخواست بیشتر از حد مجاز است.",
};
}
const rawBody = await request.text();
const actualSize =
new TextEncoder().encode(rawBody).byteLength;
if (actualSize > MAX_BODY_BYTES) {
return {
ok: false,
message: "حجم درخواست بیشتر از حد مجاز است.",
};
}
try {
return {
ok: true,
value: JSON.parse(rawBody),
};
} catch {
return {
ok: false,
message: "JSON ارسالشده معتبر نیست.",
};
}
}
async function handleChat(
request: Request,
env: Env,
stream: boolean
): Promise<Response> {
const requestId = crypto.randomUUID();
const parsed = await parseRequestBody(request);
if (!parsed.ok) {
return jsonResponse(
request,
env,
{
error: parsed.message,
request_id: requestId,
},
400,
requestId
);
}
const validation =
validateChatBody(parsed.value);
if (!validation.ok) {
return jsonResponse(
request,
env,
{
error: validation.message,
request_id: requestId,
},
422,
requestId
);
}
const controller = new AbortController();
const timeoutId = setTimeout(
() => controller.abort(),
90_000
);
try {
const upstreamResponse = await fetch(
DARVAREH_CHAT_URL,
{
method: "POST",
headers: {
"Authorization":
`Bearer ${env.DARVAREH_API_KEY}`,
"Content-Type": "application/json",
"Accept": stream
? "text/event-stream"
: "application/json",
"X-Request-ID": requestId,
},
body: JSON.stringify({
model: env.DARVAREH_MODEL_ID,
messages: [
{
role: "system",
content:
"شما یک دستیار فارسی دقیق و کاربردی هستید. پاسخ را شفاف، مسئولانه و بدون اطلاعات ساختگی ارائه کنید.",
},
...validation.body.messages,
],
temperature:
validation.body.temperature,
max_tokens:
validation.body.max_tokens,
stream,
}),
signal: controller.signal,
}
);
clearTimeout(timeoutId);
if (!upstreamResponse.ok) {
const upstreamError =
await upstreamResponse.text();
console.error(
JSON.stringify({
event: "darvareh_upstream_error",
request_id: requestId,
upstream_status:
upstreamResponse.status,
detail: upstreamError.slice(0, 1000),
})
);
return jsonResponse(
request,
env,
{
error:
"سرویس هوش مصنوعی در حال حاضر پاسخ مناسبی برنگرداند.",
request_id: requestId,
},
502,
requestId
);
}
const responseHeaders =
createCorsHeaders(request, env);
responseHeaders.set(
"Content-Type",
upstreamResponse.headers.get(
"Content-Type"
) ??
(
stream
? "text/event-stream; charset=utf-8"
: "application/json; charset=utf-8"
)
);
responseHeaders.set(
"Cache-Control",
"no-store"
);
responseHeaders.set(
"X-Request-ID",
requestId
);
if (stream) {
responseHeaders.set(
"X-Accel-Buffering",
"no"
);
}
return new Response(
upstreamResponse.body,
{
status: upstreamResponse.status,
headers: responseHeaders,
}
);
} catch (error) {
clearTimeout(timeoutId);
const isTimeout =
error instanceof Error &&
error.name === "AbortError";
console.error(
JSON.stringify({
event: "darvareh_request_failed",
request_id: requestId,
timeout: isTimeout,
message:
error instanceof Error
? error.message
: "Unknown error",
})
);
return jsonResponse(
request,
env,
{
error: isTimeout
? "زمان پردازش درخواست بیش از حد مجاز شد."
: "ارتباط با سرویس هوش مصنوعی برقرار نشد.",
request_id: requestId,
},
isTimeout ? 504 : 502,
requestId
);
}
}
export default {
async fetch(
request: Request,
env: Env
): Promise<Response> {
const url = new URL(request.url);
if (!isOriginAllowed(request, env)) {
return jsonResponse(
request,
env,
{
error: "Origin درخواست مجاز نیست.",
},
403
);
}
if (request.method === "OPTIONS") {
return new Response(
null,
{
status: 204,
headers: createCorsHeaders(
request,
env
),
}
);
}
if (
request.method === "GET" &&
url.pathname === "/"
) {
return jsonResponse(
request,
env,
{
name: "Darvareh AI Worker",
status: "running",
endpoints: [
"POST /v1/chat",
"POST /v1/chat/stream",
"GET /health",
],
}
);
}
if (
request.method === "GET" &&
url.pathname === "/health"
) {
return jsonResponse(
request,
env,
{
status: "ok",
service: "darvareh-ai-worker",
}
);
}
if (!isAuthorized(request, env)) {
return jsonResponse(
request,
env,
{
error: "دسترسی غیرمجاز است.",
},
401
);
}
if (
request.method === "POST" &&
url.pathname === "/v1/chat"
) {
return handleChat(
request,
env,
false
);
}
if (
request.method === "POST" &&
url.pathname === "/v1/chat/stream"
) {
return handleChat(
request,
env,
true
);
}
return jsonResponse(
request,
env,
{
error: "مسیر مورد نظر پیدا نشد.",
},
404
);
},
} satisfies ExportedHandler<Env>;
کد Worker چگونه کار میکند؟
دریافت درخواست
تابع اصلی Worker درخواست ورودی را دریافت میکند:
async fetch(
request: Request,
env: Env
): Promise<Response>
پارامتر request شامل اطلاعات HTTP است و env متغیرها و Secretهای Worker را در اختیار کد قرار میدهد.
بررسی مسیر
مسیر درخواست با URL API مقایسه میشود:
const url = new URL(request.url);
سپس براساس Method و Path، Handler مناسب اجرا میشود.
احراز دسترسی برنامه
Worker مقدار Header زیر را بررسی میکند:
Authorization: Bearer YOUR_APP_TOKEN
این Token با کلید درواره متفاوت است:
| مقدار | کاربرد |
|---|---|
APP_TOKEN | دسترسی Client مورد اعتماد به Worker |
DARVAREH_API_KEY | دسترسی Worker به API درواره |
کلید درواره هرگز نباید برای Client ارسال شود.
اعتبارسنجی ورودی
Worker محدودیتهای زیر را بررسی میکند:
- نوع
Content-Type - حجم Body
- معتبر بودن JSON
- تعداد پیامها
- Role پیام
- طول هر پیام
- مجموع طول پیامها
- محدوده
temperature - محدوده
max_tokens
این کنترلها باعث میشوند Client نتواند بدون محدودیت، درخواست دلخواه و پرهزینه به API بالادستی ارسال کند.
انتخاب مدل سمت سرور
Model ID از Environment خوانده میشود:
model: env.DARVAREH_MODEL_ID
در نتیجه کاربر نهایی نمیتواند با تغییر Body، مدل دیگری را انتخاب کند.
اگر محصول شما باید چند مدل داشته باشد، بهتر است یک Allowlist داخلی تعریف کنید:
const ALLOWED_MODELS = {
fast: "MODEL_ID_FAST",
quality: "MODEL_ID_QUALITY",
};
سپس کاربر فقط Aliasهایی مانند fast و quality را ارسال کند، نه Model ID آزاد.
اجرای محلی Worker
دستور زیر را اجرا کنید:
npx wrangler dev
Worker معمولاً روی آدرس زیر در دسترس قرار میگیرد:
http://localhost:8787
Health Check:
curl http://localhost:8787/health
خروجی:
{
"status": "ok",
"service": "darvareh-ai-worker"
}
تست API معمولی
curl -X POST \
http://localhost:8787/v1/chat \
-H "Authorization: Bearer YOUR_APP_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"messages": [
{
"role": "user",
"content": "Serverless را با یک مثال ساده توضیح بده."
}
],
"temperature": 0.3,
"max_tokens": 700
}'
ساختار پاسخ با پاسخ استاندارد Chat Completions سازگار خواهد بود:
{
"id": "chatcmpl-example",
"object": "chat.completion",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "در معماری Serverless..."
},
"finish_reason": "stop"
}
]
}
تست Streaming
برای مشاهده پاسخ تدریجی از گزینه -N در curl استفاده کنید:
curl -N -X POST \
http://localhost:8787/v1/chat/stream \
-H "Authorization: Bearer YOUR_APP_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"messages": [
{
"role": "user",
"content": "مزایا و محدودیتهای Cloudflare Workers را توضیح بده."
}
],
"temperature": 0.2,
"max_tokens": 1000
}'
دادهها بهتدریج دریافت میشوند:
data: {"id":"...","choices":[{"delta":{"content":"Cloudflare"}}]}
data: {"id":"...","choices":[{"delta":{"content":" Workers"}}]}
data: [DONE]
Worker بدون تبدیل پاسخ، Stream دریافتی از درواره را به Client منتقل میکند:
return new Response(
upstreamResponse.body,
{
status: upstreamResponse.status,
headers: responseHeaders,
}
);
طبق مستندات Cloudflare، اگر Worker پاسخ بالادستی را بدون خواندن کامل Body منتقل کند، جریان پاسخ حفظ میشود. همچنین میتوان برای ساخت Stream سفارشی از ReadableStream استفاده کرد. مستندات Streams در Cloudflare Workers
چرا نباید response.json را در Streaming استفاده کنیم؟
این کد برای Streaming مناسب نیست:
const data =
await upstreamResponse.json();
return Response.json(data);
متد json() منتظر میماند تا کل Body دریافت و Parse شود. بنابراین مزیت دریافت تدریجی پاسخ از بین میرود.
برای حفظ Streaming باید از Body اصلی استفاده شود:
return new Response(
upstreamResponse.body,
{
headers: {
"Content-Type": "text/event-stream",
},
}
);
تنظیم Secretهای محیط Production
Secretها را با Wrangler اضافه کنید:
npx wrangler secret put DARVAREH_API_KEY
پس از اجرای دستور، مقدار کلید را وارد کنید.
برای Token داخلی برنامه:
npx wrangler secret put APP_TOKEN
وجود secrets.required باعث میشود Wrangler پیش از Deploy، Secretهای ضروری را بررسی کند.
برای مشاهده نام Secretهای ثبتشده:
npx wrangler secret list
مقدار Secret بعد از ثبت نباید در خروجی نمایش داده شود.
استقرار Worker
برای Deploy از دستور زیر استفاده کنید:
npx wrangler deploy
بعد از استقرار، آدرسی مشابه زیر دریافت میکنید:
https://darvareh-ai-api.YOUR_SUBDOMAIN.workers.dev
Health Check نسخه منتشرشده:
curl \
https://darvareh-ai-api.YOUR_SUBDOMAIN.workers.dev/health
تست API:
curl -X POST \
https://darvareh-ai-api.YOUR_SUBDOMAIN.workers.dev/v1/chat \
-H "Authorization: Bearer YOUR_APP_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"messages": [
{
"role": "user",
"content": "Edge Computing چیست؟"
}
]
}'
مشاهده لاگ Worker
برای مشاهده لاگهای زنده:
npx wrangler tail
نمونه لاگ خطا:
{
"event": "darvareh_upstream_error",
"request_id": "5b71b492-...",
"upstream_status": 429
}
در کد پروژه، کلید API، APP Token و Authorization Header ثبت نمیشوند.
در لاگها از ذخیره اطلاعات زیر خودداری کنید:
- کلید API
- Token کاربر
- محتوای حساس پیام
- اطلاعات شخصی
- Header کامل Authorization
- Secretهای محیطی
Request ID چه کاربردی دارد؟
برای هر درخواست یک شناسه منحصربهفرد تولید میشود:
const requestId = crypto.randomUUID();
این شناسه در پاسخ قرار میگیرد:
X-Request-ID: 5b71b492-...
همچنین در خطاهای ثبتشده وجود دارد. بنابراین اگر کاربر خطایی گزارش کند، میتوانید با Request ID رخداد مربوط به همان درخواست را در لاگ پیدا کنید.
اتصال Frontend به Worker
نمونه درخواست JavaScript:
async function sendMessage(message) {
const response = await fetch(
"https://api.example.com/v1/chat",
{
method: "POST",
headers: {
"Authorization":
"Bearer YOUR_APP_TOKEN",
"Content-Type":
"application/json",
},
body: JSON.stringify({
messages: [
{
role: "user",
content: message,
},
],
temperature: 0.3,
max_tokens: 700,
}),
}
);
const data = await response.json();
if (!response.ok) {
throw new Error(
data.error ?? "Request failed"
);
}
return data.choices[0].message.content;
}
نکته مهم درباره APP_TOKEN در مرورگر
قرار دادن APP_TOKEN ثابت در JavaScript عمومی، آن را محرمانه نگه نمیدارد. کاربران میتوانند Token را در Source یا Network مرورگر مشاهده کنند.
روش APP_TOKEN برای این موارد مناسبتر است:
- ارتباط Server-to-Server
- ابزار داخلی
- تست API
- Backend مورد اعتماد
- Jobهای خودکار
برای اپلیکیشن عمومی باید یکی از روشهای مناسب مدیریت کاربر پیادهسازی شود:
- Session سمت سرور
- Token کوتاهعمر برای هر کاربر
- احراز هویت کاربران
- بررسی سطح دسترسی
- محدودیت مصرف برای هر حساب
- ذخیره Usage در پایگاه داده
CORS نیز ابزار احراز هویت نیست. CORS رفتار مرورگر را کنترل میکند، اما مانع ارسال مستقیم درخواست با curl یا یک سرور دیگر نمیشود.
اتصال دامنه اختصاصی
اگر دامنه شما بهعنوان Zone فعال در Cloudflare مدیریت میشود، میتوانید یک Custom Domain به Worker متصل کنید.
در Dashboard مسیر زیر را باز کنید:
Workers & Pages
Worker
Settings
Domains & Routes
Add
Custom Domain
سپس دامنهای مانند این وارد کنید:
ai-api.example.com
در فایل wrangler.jsonc نیز میتوان Route تعریف کرد:
{
"routes": [
{
"pattern": "ai-api.example.com",
"custom_domain": true
}
]
}
Cloudflare برای Custom Domain رکورد موردنیاز و گواهی مرتبط را مدیریت میکند. دامنه باید داخل یک Zone فعال Cloudflare باشد و روی همان Hostname رکورد ناسازگار موجود نباشد. مستندات Custom Domains در Cloudflare Workers
اگر DNS دامنه شما در ارائهدهنده دیگری مدیریت میشود، قبل از طراحی اتصال مستقیم Custom Domain باید محدودیتهای این روش را بررسی کنید. در چنین شرایطی میتوانید از آدرس workers.dev استفاده کنید یا یک معماری Reverse Proxy سازگار با DNS و زیرساخت خود بسازید.
مدیریت CORS برای چند دامنه
در پروژه فعلی فقط یک Origin مجاز است:
origin === env.ALLOWED_ORIGIN
اگر چند Frontend دارید، Allowlist تعریف کنید:
const allowedOrigins = new Set([
"https://app.example.com",
"https://admin.example.com",
"http://localhost:3000",
]);
function isAllowedOrigin(
origin: string | null
): boolean {
if (!origin) {
return true;
}
return allowedOrigins.has(origin);
}
از بازتاب دادن هر Origin به شکل زیر خودداری کنید:
headers.set(
"Access-Control-Allow-Origin",
request.headers.get("Origin")!
);
Origin باید ابتدا با Allowlist مورد اعتماد مقایسه شود.
مدیریت Timeout
در پروژه یک Timeout برای درخواست بالادستی تعریف شده است:
const controller =
new AbortController();
const timeoutId = setTimeout(
() => controller.abort(),
90_000
);
سپس Signal به fetch داده میشود:
signal: controller.signal
اگر API در زمان تعیینشده پاسخی برنگرداند، درخواست لغو میشود و Worker خطای کنترلشده برمیگرداند:
{
"error": "زمان پردازش درخواست بیش از حد مجاز شد.",
"request_id": "..."
}
Timeout مناسب به کاربرد شما وابسته است. برای پاسخ کوتاه ممکن است ۳۰ تا ۶۰ ثانیه کافی باشد، اما بعضی مدلها یا خروجیهای طولانی به زمان بیشتری نیاز دارند.
Timeout را براساس دادههای واقعی و محدودیتهای جاری پلتفرم انتخاب کنید.
جلوگیری از تبدیل Worker به API عمومی پرهزینه
اگر Worker بدون کنترل روی اینترنت منتشر شود، افراد دیگر میتوانند از اعتبار درواره شما مصرف کنند.
حداقل کنترلهای پیشنهادی:
- احراز هویت درخواست
- محدودیت طول ورودی
- محدودیت تعداد پیام
- محدودیت تعداد توکن خروجی
- انتخاب مدل در Backend
- ثبت Usage
- محدودیت درخواست برای هر کاربر
- بررسی موجودی یا سهمیه
- ثبت Request ID
- Timeout
- محدود کردن Origin برای مرورگر
- مخفی نگه داشتن کلید درواره
برای نسخه عمومی و چندکاربره، یک Token ثابت کافی نیست. باید هویت و سهمیه هر کاربر بهصورت مستقل مدیریت شود.
افزودن Rate Limiting
برای یک محصول عمومی بهتر است محدودیتهایی مشابه زیر تعریف شوند:
کاربر عادی:
10 درخواست در دقیقه
100 درخواست در روز
کاربر حرفهای:
60 درخواست در دقیقه
2000 درخواست در روز
نباید شمارنده Rate Limit را فقط داخل یک متغیر معمولی Worker ذخیره کنید:
let requestCount = 0;
Instanceهای Worker میتوانند متعدد و موقتی باشند و حافظه محلی آنها منبع داده پایدار و مشترک نیست.
برای Rate Limiting قابل اتکا باید از قابلیتهای مناسب پلتفرم یا یک ذخیرهساز مشترک استفاده شود.
مدیریت تاریخچه گفتگو
Worker پروژه فعلی Stateless است. Client تاریخچه را در آرایه messages ارسال میکند:
{
"messages": [
{
"role": "user",
"content": "Serverless چیست؟"
},
{
"role": "assistant",
"content": "Serverless یک مدل اجرای..."
},
{
"role": "user",
"content": "یک مثال هم بزن."
}
]
}
برای محصول واقعی میتوانید تاریخچه را در یک پایگاه داده ذخیره کنید و در هر درخواست فقط شناسه Conversation را بفرستید.
نکته مهم این است که با طولانی شدن تاریخچه:
- تعداد توکن ورودی افزایش پیدا میکند.
- هزینه درخواست بیشتر میشود.
- زمان پاسخ ممکن است افزایش پیدا کند.
- Context Window بیشتری مصرف میشود.
- احتمال ورود اطلاعات نامرتبط بالا میرود.
بهتر است پیامهای قدیمی خلاصه یا براساس نیاز انتخاب شوند.
آیا پاسخ هوش مصنوعی را Cache کنیم؟
Cache کردن پاسخ هوش مصنوعی همیشه مناسب نیست.
پاسخهای زیر معمولاً نباید بهصورت عمومی Cache شوند:
- پاسخ شخصی کاربر
- محتوای دارای اطلاعات حساس
- مکالمات حساب کاربری
- خروجی وابسته به تاریخچه
- درخواستهای متفاوت با متن مشابه
- پاسخهای متغیر
برای بعضی کاربردهای عمومی و قطعی، Cache ممکن است مفید باشد:
- توضیح ثابت یک اصطلاح
- دستهبندی از پیش محاسبهشده
- خلاصه یک سند عمومی ثابت
- پاسخ با ورودی Normalized
- خروجی با Temperature پایین
اگر Cache میسازید، Cache Key باید همه پارامترهای مؤثر را در نظر بگیرد:
model
messages
temperature
max_tokens
system_prompt_version
مدیریت خطاهای API درواره
Worker نباید همه جزئیات خطای Upstream را مستقیماً به کاربر نمایش دهد.
در پروژه، جزئیات فنی در Log ثبت و یک پیام عمومی به Client ارسال میشود:
{
"error": "سرویس هوش مصنوعی در حال حاضر پاسخ مناسبی برنگرداند.",
"request_id": "..."
}
کدهای متداول:
| وضعیت | مفهوم احتمالی |
|---|---|
400 | ورودی نامعتبر |
401 | Token برنامه نامعتبر |
403 | Origin مجاز نیست |
404 | مسیر پیدا نشده |
422 | ساختار پیام یا پارامتر نامعتبر |
502 | خطا در ارتباط با سرویس بالادستی |
504 | پایان زمان انتظار |
جزئیات دقیق خطا باید با مستندات جاری سرویس و لاگ درخواست بررسی شود.
تست با JavaScript
فایل ساده test.mjs بسازید:
const response = await fetch(
"http://localhost:8787/v1/chat",
{
method: "POST",
headers: {
"Authorization":
`Bearer ${process.env.APP_TOKEN}`,
"Content-Type":
"application/json",
},
body: JSON.stringify({
messages: [
{
role: "user",
content:
"سه مزیت معماری Serverless را بنویس.",
},
],
temperature: 0.2,
max_tokens: 500,
}),
}
);
const data = await response.json();
console.log(
JSON.stringify(data, null, 2)
);
اجرا در Linux یا macOS:
APP_TOKEN="YOUR_APP_TOKEN" \
node test.mjs
تست ورودی نامعتبر
بدون Authorization:
curl -X POST \
http://localhost:8787/v1/chat \
-H "Content-Type: application/json" \
-d '{"messages":[]}'
خروجی:
{
"error": "دسترسی غیرمجاز است."
}
پیام خالی:
curl -X POST \
http://localhost:8787/v1/chat \
-H "Authorization: Bearer YOUR_APP_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"messages": [
{
"role": "user",
"content": ""
}
]
}'
خروجی:
{
"error": "محتوای پیام نمیتواند خالی باشد.",
"request_id": "..."
}
تعداد توکن بیشتر از حد مجاز:
curl -X POST \
http://localhost:8787/v1/chat \
-H "Authorization: Bearer YOUR_APP_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"messages": [
{
"role": "user",
"content": "یک پاسخ بنویس."
}
],
"max_tokens": 10000
}'
خروجی:
{
"error": "مقدار max_tokens باید بین ۱ و ۲۰۰۰ باشد.",
"request_id": "..."
}
خطاهای رایج Cloudflare Workers
Secret در env وجود ندارد
اگر با خطایی مرتبط با DARVAREH_API_KEY روبهرو شدید، بررسی کنید فایل .dev.vars در Root پروژه قرار دارد.
برای Production:
npx wrangler secret list
در صورت نبود Secret:
npx wrangler secret put DARVAREH_API_KEY
خطای 401 از Worker
Header زیر ارسال نشده یا مقدار آن اشتباه است:
Authorization: Bearer YOUR_APP_TOKEN
توجه کنید APP_TOKEN با DARVAREH_API_KEY تفاوت دارد.
خطای 401 از API بالادستی
کلید درواره ممکن است اشتباه، غیرفعال یا بهدرستی در Secret ذخیره نشده باشد.
Secret را دوباره تنظیم کنید:
npx wrangler secret put DARVAREH_API_KEY
خطای CORS در مرورگر
موارد زیر را بررسی کنید:
- مقدار
ALLOWED_ORIGIN - وجود
httpsیاhttp - وجود یا نبود پورت
- پاسخ صحیح به درخواست
OPTIONS - Headerهای مجاز
- دامنه واقعی Frontend
این دو Origin با هم متفاوتاند:
https://app.example.com
http://app.example.com
این دو نیز متفاوتاند:
http://localhost:3000
http://localhost:5173
Streaming یکجا نمایش داده میشود
مطمئن شوید Body پاسخ بالادستی را با json() یا text() نخواندهاید.
روش مناسب:
return new Response(
upstreamResponse.body,
{
headers: {
"Content-Type": "text/event-stream",
},
}
);
برای تست با curl:
curl -N ...
Worker روی دامنه اختصاصی اجرا نمیشود
بررسی کنید:
- دامنه در Zone فعال Cloudflare باشد.
- Custom Domain به Worker درست متصل شده باشد.
- رکورد ناسازگار روی همان Hostname وجود نداشته باشد.
- دامنه دقیقاً با Hostname تعریفشده یکسان باشد.
- Deploy جدید انجام شده باشد.
چکلیست انتشار Production
پیش از انتشار Worker، موارد زیر را بررسی کنید:
- کلید درواره فقط در Secret ذخیره شده باشد.
- فایل
.dev.varsداخل Git نباشد. - Model ID سمت Worker انتخاب شود.
- تعداد توکن خروجی محدود شده باشد.
- طول پیامها محدود شده باشد.
- حجم Body محدود شده باشد.
- Timeout تعریف شده باشد.
- خطای Upstream مدیریت شود.
- Request ID تولید شود.
- اطلاعات محرمانه داخل Log ثبت نشود.
- CORS فقط برای Originهای مورد اعتماد فعال باشد.
- API عمومی دارای احراز هویت واقعی باشد.
- Rate Limit برای هر کاربر تعریف شود.
- مصرف و هزینه درخواستها پایش شود.
- Streaming در محیط واقعی آزمایش شود.
- محدودیتهای فعلی پلن Cloudflare بررسی شود.
- قیمت و محدودیت مدل انتخابی بررسی شود.
پرسشهای متداول
آیا Cloudflare Workers رایگان است؟
Cloudflare معمولاً پلن رایگان با محدودیت مشخص ارائه میدهد، اما مقدار دقیق محدودیتها و امکانات ممکن است تغییر کند. برای تصمیمگیری، صفحه رسمی قیمتگذاری و محدودیتها را بررسی کنید.
آیا برای اجرای Worker به سرور نیاز داریم؟
نیازی به تهیه و مدیریت مستقیم VPS ندارید. اجرای Worker توسط زیرساخت Cloudflare مدیریت میشود.
آیا میتوان Python را روی Cloudflare Workers اجرا کرد؟
Cloudflare Workers در درجه اول با JavaScript و TypeScript شناخته میشود و قابلیتهای Runtime آن در حال توسعه است. برای پروژهای که به کتابخانههای کامل Python یا وابستگیهای سیستمی نیاز دارد، باید سازگاری فعلی Runtime را بررسی کنید. در این آموزش TypeScript انتخاب شده چون مسیر ساده و رایجی برای Worker است.
آیا میتوان از Worker به API درواره وصل شد؟
بله. Worker میتواند با fetch به آدرس زیر درخواست ارسال کند:
https://api.darvareh.ir/v1/chat/completions
کلید API باید در Secret نگهداری شود.
آیا Cloudflare Worker جای FastAPI را میگیرد؟
همیشه نه. Worker برای APIهای سبک، واسطهای Serverless و پردازش Edge بسیار مناسب است. FastAPI برای برنامههای Python، منطق پیچیده Backend، کتابخانههای تخصصی و معماریهای دارای Processهای طولانی انعطاف بیشتری دارد.
آیا میتوان تاریخچه چت را داخل Worker ذخیره کرد؟
نه در متغیر معمولی و حافظه موقت Worker. برای ذخیره پایدار تاریخچه باید از یک پایگاه داده یا Storage مناسب استفاده کنید.
آیا میتوان پاسخ API درواره را Stream کرد؟
بله. کافی است درخواست با stream: true ارسال و upstreamResponse.body بدون خواندن کامل به Client منتقل شود.
آیا APP_TOKEN برای اپلیکیشن عمومی کافی است؟
خیر. Token ثابت در مرورگر قابل مشاهده است. اپلیکیشن عمومی باید احراز هویت، Token کوتاهعمر و سهمیه مستقل هر کاربر داشته باشد.
آیا میتوان چند مدل را از طریق یک Worker ارائه داد؟
بله، اما بهتر است مدلها در یک Allowlist سمت سرور تعریف شوند. اجازه ندهید کاربر هر Model ID دلخواهی ارسال کند.
قیمت استفاده از مدلها چقدر است؟
هزینه به مدل انتخابی، تعداد توکن ورودی، تعداد توکن خروجی و نوع درخواست بستگی دارد. برای مشاهده اطلاعات بهروز، صفحه مدلها و قیمتهای درواره را بررسی کنید.
جمعبندی
Cloudflare Workers راهی کاربردی برای ساخت APIهای Serverless و اجرای منطق Backend بدون مدیریت مستقیم سرور است. این پلتفرم میتواند درخواست کاربر را دریافت کند، ورودی را بررسی کند و آن را به API هوش مصنوعی درواره بفرستد.
در پروژه این مقاله قابلیتهای زیر را پیادهسازی کردیم:
- ساخت Worker با TypeScript
- اجرای محلی با Wrangler
- ساخت API چت
- اتصال به API درواره
- نگهداری کلید در Secret
- محافظت از Endpoint با Token
- محدود کردن ورودی
- کنترل Model ID
- مدیریت CORS
- افزودن Timeout
- ثبت Request ID
- مدیریت خطاهای Upstream
- انتقال پاسخ Streaming
- Deploy روی
workers.dev - اتصال Custom Domain
برای یک نمونه آزمایشی یا API داخلی، این معماری میتواند با سرعت و پیچیدگی کم راهاندازی شود. برای محصول عمومی باید مدیریت کاربران، Rate Limiting، سهمیه مصرف، ثبت Usage و ذخیرهسازی پایدار نیز به آن اضافه شود.
برای دریافت کلید API و شروع ساخت سرویس هوش مصنوعی، وارد درواره شوید. برای انتخاب مدل مناسب و مشاهده قیمتهای فعلی نیز صفحه مدلهای درواره را ببینید.
منابع
- راهنمای رسمی شروع Cloudflare Workers
- مستندات Wrangler
- مدیریت Secret در Cloudflare Workers
- مستندات Streaming و Streams API
- اتصال دامنه اختصاصی به Worker
- محدودیتهای Cloudflare Workers
مقالات مرتبط
- آموزش اتصال API هوش مصنوعی به اپلیکیشن
- ساخت API آماده Production برای هوش مصنوعی
- آموزش Streaming API در هوش مصنوعی
- API Gateway چیست؟ راهنمای کامل دروازه API
- آموزش دریافت API Key هوش مصنوعی
- آموزش API درواره با cURL
- مانیتورینگ و Observability سرویسهای هوش مصنوعی
برای مطالعه شرایط استفاده و محدودیتهای مسئولیت، صفحه «سلب مسئولیت» را مشاهده کنید.