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من حزمة VercelaiSDK مع@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). إذا كان الربط غير مضبوط أو فارغًا رُفض كل طلب (الإغلاق عند الفشل)؛ والإدخال الذي ليس اسم مضيف يُفشل كل طلب بخطأ يسمّيه.
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):
<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. ويُفشل استيراد أي اسم آخر البناء مع عرض هذه القائمة؛ أما استيرادات الأنواع فقط فتعمل لكل نوع.
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.