Skip to main content
يولّد الأمر npx lousho build --target=cloudflare-worker --agent=agent.yaml ملف Worker بصيغة وحدة (worker.ts) وملف wrangler.toml وحزمة dist/worker.js يفحصها البناء بحثًا عن استيرادات node:. يتطلب تثبيت tsup وwrangler، ويقبل ملف مواصفات وكيل أو مجلد وكيل (npx lousho build ./my-agent --target=cloudflare-worker). يقدّم الـ Worker واجهة HTTP API نفسها التي تقدّمها الأهداف الأخرى. هذا الهدف هو الأكثر تقييدًا بين الأهداف الثلاثة، لذلك نبدأ بحدوده.

ما الذي يعمل وما الذي لا يعمل

يرفض lousho build أي مواصفات يكون مزوّدها أو أداتها خارج عمود Worker، وأي مجلد وكيل فيه مجلد أو إعداد يخلو منه عمود Worker، ويُصدر الخطأ LOUSHO_DEPLOY_FAILED الذي يسمّي العنصر المرفوض ويقترح --target=node-server أو --target=docker.

المزوّدون والأدوات

أسباب الصفوف الخاصة بالمزوّدين والأدوات:
  • المزوّدون: mock وopenai وanthropic وopenrouter. المزوّدون الحقيقيون مبنيون على الدالتين generateText/streamText من حزمة Vercel ai SDK مع @ai-sdk/openai/@ai-sdk/anthropic (وopenrouter هو @ai-sdk/openai موجَّه إلى https://openrouter.ai/api/v1)، وهي تنفيذات تعتمد على fetch() ومعايير الويب فقط دون أي استيراد node:*، فتُجمَّع وتعمل على Workers بسلاسة؛ ويتحقق فحص التسرّب في البناء من كل حزمة. المزوّد ollama غير مدعوم هنا: فنقطته الافتراضية محلية http://localhost:11434 ولا يستطيع الـ Worker الوصول إليها؛ استخدم node-server أو docker معه؛
  • الأدوات: current-date وday-name وhttp. الأداة web-fetch غير مدعومة. على Node ترفض http وweb-fetch الوجهات الخاصة بإجراء استعلام DNS خاص بهما (node:dns داخل Agent من undici) يفحص كل عنوان يُحلَّ إليه المضيف ويتصل بالعنوان الذي فحصه، فلا يستطيع هجوم DNS rebinding تجاوز الفحص. أما الـ Worker فلا يملك أيًّا من ذلك: إذ يحلّ fetch() الأسماء داخل شبكة Cloudflare ولا يتيح أي نقطة وصول لرؤية العنوان أو تثبيته. وما يستطيع الـ Worker فحصه هو عنوان URL (البروتوكول، واسم المضيف، والمضيف بصيغة عنوان IP)؛ أما ما لا يستطيع ضمانه فهو العنوان الذي يتصل به اسم المضيف، فالاسم الذي يُحلَّ إلى عنوان داخلي لن يُكتشف. لذلك فإن http في الـ Worker (باسم الأداة http_request ومدخلاتها نفسها كما على Node) أداة مختلفة: فهي لا تصل إلا إلى أسماء المضيفين التي تدرجها. وفي أول عنوان URL وعند كل إعادة توجيه ترفض:
    • أي بروتوكول غير http: وhttps:؛
    • مضيفًا بصيغة عنوان IP (http://10.0.0.1/ وhttp://[::1]/)، حتى لو كان مدرجًا: لا يوجد فحص للعناوين، لذا لا يُسمح إلا بالأسماء؛
    • مضيفًا لا يطابق الربط LOUSHO_HTTP_ALLOW، وهو قائمة مفصولة بفواصل من أسماء المضيفين (api.github.com) وبدائل *. العامة (*.example.com، وهي تطابق النطاقات الفرعية فقط ولا تطابق example.com). إذا كان الربط غير مضبوط أو فارغًا رُفض كل طلب (الإغلاق عند الفشل)؛ والإدخال الذي ليس اسم مضيف يُفشل كل طلب بخطأ يسمّيه.
    وبذلك لا يستطيع النموذج اختيار مضيف عشوائي ولا مضيف يستغل DNS rebinding، لأن الأسماء التي أدرجتها وحدها تمرّ؛ والاسم المدرج موثوق أينما حُلَّ، فلا تُدرج إلا مضيفين تتحكم بهم أو تثق بهم. يُتحقَّق من TLS دائمًا (لا يوجد خيار validateSSL) وتعمل الأداة دون بيئة معزولة. أما أداة من كتابتك تستدعي fetch() داخل Worker فلا تحصل من الـ SDK على أي حماية من SSRF.

البناء والنشر

يولّد البناء Worker بصيغة وحدة (export default { fetch }) ويجمّعه كوحدة ES لمنصة المتصفح؛ ويفشل البناء إذا انتهى أي استيراد node: داخل dist/worker.js. يشير wrangler.toml بالحقل main إلى dist/worker.js مع no_bundle = true، فيُرفع بالضبط الملف الذي جرى التحقق منه. يُبنى ويعمل مع ai v4 (التثبيت الافتراضي) أو ai v7 (مع @ai-sdk/openai/@ai-sdk/anthropic v4) دون أي علَم توافق (أي دون nodejs_compat):
تُقرأ مفاتيح API الخاصة بالمزوّدين من ارتباطات Worker المسمّاة <TYPE>_API_KEY (مثل wrangler secret put OPENAI_API_KEY أو wrangler secret put ANTHROPIC_API_KEY أو wrangler secret put OPENROUTER_API_KEY). ويجب تثبيت الحزم النظيرة (@ai-sdk/openai لكل من openai وopenrouter، و@ai-sdk/anthropic لـ anthropic، وai) إلى جانب @lousho/build-ai-agent كي يجمّعها lousho build. مجلدات الوكيل على Worker. لا يملك الـ Worker نظام ملفات ولا يستطيع استيراد ملف بمساره وقت التشغيل، لذلك يقرأ البناء مجلد الوكيل على Node ويكتب agent.module.ts: استيرادًا ثابتًا لكل ملف tools/*.ts (ولإعدادات agent.ts / agent.js إن وُجدت)، مع نسخ instructions.md وإعدادات JSON/YAML والمهارات بصيغة JSON. ويبني الـ Worker الوكيل بالقواعد نفسها المتبعة في resolveAgentDir() (مفاتيح الإعدادات، وصادرات الأدوات، وأسماء الأدوات المكررة):
  • يجب أن يسمّي model في الإعدادات مزوّدًا من الجدول أعلاه ("model": "openai/gpt-4o-mini")، مع مفتاحه في الربط OPENAI_API_KEY؛ أو أن يضبط agent.ts الخاصية provider على نسخة. لا توجد بيئة يُختار منها نموذج افتراضي، لذلك يُرفض المجلد الذي يخلو من الأمرين.
  • يفحص lousho build إعدادات JSON/YAML؛ أما إعدادات agent.ts فتُفحص عند بدء الـ Worker، فيُبلغ wrangler deploy عن المشكلة.
  • تُرفض subagents/ وschedules/ وchannels/ وmemory/ وprojectInstructions، مع تسمية المجلد أو المفتاح. استخدم node-server أو docker لها.
  • تُجمَّع ملفات الأدوات من مواضعها، فتُحلّ استيراداتها النسبية و node_modules الخاصة بها كما في التطوير. والأداة التي تستورد وحدة Node مدمجة تُفشل فحص التسرّب في البناء، الذي يسمّي الملف.
  • داخل أداة أو agent.ts، تكون @lousho/build-ai-agent مجموعة جزئية من الحزمة آمنة للـ Worker: defineTool وisDefinedTool وalways وnever وonce وdefineSkill وcreateMockProvider و MockLLMProvider وOpenAIProvider وAnthropicProvider وOpenRouterProvider وfromAiSdk و LLMProviderRegistry وtextOf وSDKError وConfigurationError وToolExecutionError و ValidationError. ويُفشل استيراد أي اسم آخر البناء مع عرض هذه القائمة؛ أما استيرادات الأنواع فقط فتعمل لكل نوع.
ما الذي تصل إليه الأداة على Worker: يعمل ملف الأداة داخل عزل (isolate) الـ Worker، لا في بيئة معزولة، وبالصلاحيات نفسها التي يملكها بقية الـ Worker. يمكنه استدعاء fetch() لأي مضيف (إذ يقيّد LOUSHO_HTTP_ALLOW أداة http المضمّنة فقط، لا شيفرتك)، ويشارك ذاكرة العزل مع كل جلسة يخدمها العزل (وبدون AGENT_CHECKPOINTS يشمل ذلك جلساتها). لا يمرّر الـ SDK إلى الأداة الكائن env الخاص بالـ Worker (مفاتيح API الخاصة بالمزوّدين وLOUSHO_API_TOKEN ومساحة أسماء KV)، ولا يحلّ البناء استيرادات cloudflare:، فيفشل البناء عند import { env } from 'cloudflare:workers'؛ لكن الشيفرة داخل عزل واحد ليست حدًّا أمنيًا. لا تنشر إلا شيفرة أدوات تثق بها لتصل إلى هذه الارتباطات.

الارتباطات

يقدّم الـ Worker HTTP API: GET /health، وPOST /chat مبثوثًا بصيغة SSE (ReadableStream)، وGET /chat/:sessionId، ونقطة نهاية الموافقات، وجسم { "message" } المُهمَل. ارتباطاته: يُولَّد wrangler.toml وكتلة [[kv_namespaces]] الخاصة بـ AGENT_CHECKPOINTS معلَّقة (مُحوَّلة إلى تعليق)، مع أوامر إنشاء مساحة الأسماء (npx wrangler kv namespace create AGENT_CHECKPOINTS، إضافةً إلى صيغة --preview) ومكان لصق المعرّفات الناتجة. أزل التعليق عنها واملأ المعرّفات. وإذا أدرجت المواصفات http فسيحتوي الملف أيضًا على كتلة [vars] معلَّقة من أجل LOUSHO_HTTP_ALLOW؛ أزل التعليق عنها وأدرج مضيفيك:

الجلسات ونقاط الحفظ والموافقات

KVStore(kvBinding, { prefix?, ttl?, historyLimit? }) هو AgentStore الذي يبنيه الـ Worker المولَّد من الارتباط. أما الـ Worker المكتوب يدويًا فيستورده من المسار الفرعي /kv الذي لا يحتوي أي استيراد node:* في أي موضع من رسمه البياني، مع تعريف الارتباط بالنوع KVBinding (جزء get/put/delete من KVNamespace في Cloudflare، فلا حاجة إلى @cloudflare/workers-types):
يصدّر /kv أيضًا KVCheckpointStore (لنقاط الحفظ فقط) و CHECKPOINT_KV_BINDING ('AGENT_CHECKPOINTS'، وهو اسم الارتباط الذي يقرؤه الـ Worker المولَّد). مفاتيح KVStore، وقبل كل منها prefix اختياري: يجعل ttl: { sessions?, checkpoints?, approvals? } (بالثواني، ويقبل KV ستين ثانية فأكثر) كل نوع من السجلات ينتهي بعد هذه المدة من آخر كتابة؛ وافتراضيًا تبقى السجلات حتى تُحذف. يمكن البتّ في موافقة أوقفت دورة في طلب ما، بطلب لاحق على عزل آخر: فالجلسة التي لها نقطة حفظ تسمّي موافقتها المعلّقة، وتتابعها نقطة نهاية الموافقات من KV. وأحداث هذه المتابعة تتخطى tool.done لاستدعاء الأداة الذي بُتّ فيه؛ ويحمل run.done فيها النص النهائي. يحتفظ POST /chat { "message", "sessionId"? } المُهمَل بسلوكه السابق على Workers: فمع sessionId تُحفظ نقطة حفظ للتشغيل في checkpoints/<sessionId> بعد كل نتيجة أداة، ويستعيدها طلب لاحق يعيد استخدام sessionId (بعد عطل أو إعادة تدوير للعزل).

التشغيلات المجدولة

تتحول مُشغِّلات cron في المواصفات (triggers: [{ type: 'cron', cron: '0 9 * * MON', input: '...' }]) إلى [triggers] crons = [...] في wrangler.toml، ويصدّر الـ Worker المولَّد معالجًا scheduled() يشغّلها بوصفها دورات للوكيل (الجلسة schedule:<name>، راجع الجداول الزمنية). تقيّم Cloudflare التعبيرات بتوقيت UTC وبدقة دقيقة واحدة؛ ويرفض البناء timezone أو حقل الثواني أو الاختصار @daily أو يوم الأسبوع الرقمي (LOUSHO_SCHEDULE_INVALID). وفي Worker تكتبه بنفسك، اربط وكيلًا معرَّفًا في الشيفرة عبر handleScheduled(agent, schedules, controller, ctx) (وهي مُصدَّرة أيضًا من @lousho/build-ai-agent/deploy-runtime-worker). تشغّل الجداول التي يساوي cron فيها controller.cron داخل ctx.waitUntil() ولا ترمي أي استثناء؛ وعليك أنت إدراج التعبيرات نفسها تحت [triggers] crons:

الاتساق

عمر الطلب في Worker أقصر من أن يلائم CheckpointStore في الذاكرة أو المعتمد على نظام الملفات (راجع الإعدادات / src/execution/checkpoint.ts لمعرفة ما هو CheckpointStore ولماذا يحتاج التشغيل إليه كي ينجو من عطل أو من توقف عند بوابة موافقة). وAGENT_CHECKPOINTS هو ذلك المخزن، مدعومًا بـ KV (KVCheckpointStore)، ويكفي الارتباط الواحد أعلاه لتفعيله. لماذا KV لا D1 ولا Durable Objects: إن Checkpoint كتلة JSON واحدة مفتاحها sessionId، تُقرأ وتُكتب كاملة، وهذا بالضبط الشكل الذي صُمّم له Workers KV، دون أي بنية تحتية إضافية سوى ارتباط بمساحة أسماء. أما D1 فستوفّر قدرة استعلام علائقية لا يحتاجها هذا المخزن؛ وأما Durable Object فستوفّر اتساقًا صارمًا لكل جلسة على حساب إعداد صنف DO وترحيله ودفع كلفة كائن ذي حالة لكل جلسة. فإذا كان حملك يحتاج فعلًا إلى اتساق صارم للقراءة بعد الكتابة عبر مواقع الحافة (راجع التحفظ أدناه)، فإن CheckpointStore المدعوم بـ Durable Object هو مسار الترقية الطبيعي، وذلك بتنفيذ واجهة CheckpointStore نفسها (save/load/delete) فوق مساحة أسماء Durable Object بدل مساحة أسماء KV. الاتساق النهائي، فاقرأ هذا قبل الاعتماد عليه في مسارات عمل الموافقات: إن Workers KV مخزن نهائي الاتساق. تكون عملية put() مرئية فورًا لموقع الحافة الذي كتبها، لكنها قد تستغرق حتى نحو 60 ثانية لتنتشر إلى بقية مواقع حافة Cloudflare عالميًا. وعمليًا يعني هذا: إذا كُتبت نقطة حفظ جلسة في موقع حافة ووصل بعدها بقليل طلب لاحق للجلسة نفسها إلى موقع حافة مختلف، فقد يرى ذلك الطلب بيانات قديمة (نقطة حفظ أقدم، أو لا شيء) لا ما كُتب للتو. ويتجلى ذلك أكثر عند التوقفات المرتبطة بالموافقة، إذ يكون التوقف والاستئناف الذي تطلقه موافقة الإنسان لاحقًا طلبين منفصلين بطبيعتهما قد يصلان إلى موقعين مختلفين. لا يَعِد هذا الـ SDK، ولا يستطيع أن يَعِد، في ظل ضمانات KV، باتساق صارم للقراءة بعد الكتابة هنا. فإن كان مسار الموافقة لديك لا يحتمل هذه النافذة، فوجّه طلبات الجلسة الواحدة إلى موقع Cloudflare واحد بنفسك (مثلًا عبر توجيه الطلبات القائم على Durable Object) أو استخدم مخزنًا قوي الاتساق بدلًا من AGENT_CHECKPOINTS/KV. وينطبق الأمر نفسه على الجلسات: فطلبان من الجلسة نفسها في اللحظة نفسها قد يكتب أحدهما فوق دورة الآخر، لأن القراءة ثم التعديل ثم الكتابة في KV ليست ذرّية.

حجم الحزمة ووحدات Node المدمجة

المخازن المدعومة بـ KV (KVStore وKVCheckpointStore وCHECKPOINT_KV_BINDING، المصدَّرة من @lousho/build-ai-agent/kv) لا تحتوي أي إشارة إلى node:* في أي موضع من رسم اعتمادياتها. يشغّل الـ Worker المواصفات بوصفها وكيل createAgent()، ويوجّه البناء استيراداته الخاصة بـ Node فقط (تعليمات المشروع، ومخزن الجلسات في الملفات، وتصحيحات حواجز الحماية، وMCP عبر stdio) إلى طبقة بديلة تفشل عند استخدامها (src/deploy/shims/node.worker.ts). ثم تُفحص حزمة dist/worker.js المبنية بحثًا عن معرّفات node: ومعرّفات وحدات Node المدمجة المجرّدة ضمن lousho build، ويفشل البناء إن وُجد أي منها؛ وحين يكون أحدها قد استُورد (من أداة في مجلد وكيل مثلًا) يسمّي الخطأ الملف المستورِد. هناك استثناء واحد: فـ ai v7 و @ai-sdk/provider-utils v5 يبحثان عن node:module وnode:dns و node:diagnostics_channel وnode:async_hooks وقت التشغيل عبر process.getBuiltinModule()، ولا يفعلان ذلك إلا عند اكتشاف Node، ويعودان إلى fetch() (أو يتخطيان تتبّع القياس) في غيره. وتُقبل هذه المعرّفات الأربعة وسيطًا لاستدعاء كهذا ولا تُقبل في أي موضع آخر. والحزمة أكبر من Worker بدورة واحدة (نحو 1.7 MB خامًا و340 KB مضغوطة بـ gzip لوكيل mock على ai v4؛ ونحو 3.4 MB خامًا و630 KB مضغوطة على ai v7): يبلغ lousho build عن حجمها، وتقارنه describe() بحد حجم السكربت في Cloudflare.