# امنح وكيل البرمجة لديك عيونًا: مهارات Cloudflare وخادم Observability MCP ونهج TDD المحلي أولًا
Table of Contents
وكلاء البرمجة لا يعرفون التعب وسريعون إلى أبعد حد — لكنهم عُميان افتراضيًا. هذه التدوينة تتناول حلقتَي التغذية الراجعة اللتين ربطتُهما بمشروعي القائم على Cloudflare Workers حتى يستطيع Claude Code أن يرى ما يفعله الكود فعلًا: سجلات إنتاج يمكنه الاستعلام عنها بنفسه، وحزمة اختبارات محلية تحاكي المنصة بأكملها — Durable Objects وSQLite وR2 وواجهات برمجة التطبيقات الخارجية — في ثوانٍ معدودة. إنه أقرب شيء وجدته إلى الحل السحري في البرمجة الوكيلية.
النحّات الأعمى
شاهدت مؤخرًا فيديو لسلفاتوري سانفيليبو (antirez) — “Il trucco decisivo (davvero) per lavorare coi coding agent” — يصوغ بالكلمات شيئًا كنت أحوم حوله منذ شهور. الفضل كله له في هذا التأطير؛ وإن كنت تفهم الإيطالية فاذهب وشاهده.
حجّته تسير على النحو التالي. لقد سمعت كل النصائح المعتادة عن وكلاء البرمجة: اكتب مواصفات دقيقة، وشارك حدسك التصميمي بلغة غير مُلزِمة، وحافظ على نظافة قاعدة الكود، وعلّق على التوترات في الكود لا على آلياته فحسب. كل ذلك صحيح، وكله مفيد. لكن هناك خاصية واحدة في وكلاء النماذج اللغوية لا يكاد أحد يتحدث عنها، وهي التي تغيّر كل شيء: المثابرة. الوكيل سيحاول، ثم يعيد المحاولة، ثم يعيدها مرة أخرى، بسرعة لا يجاريها أي إنسان. كل محاولة فاشلة تكلّفه ثوانيَ، لا أمسيةً كاملة من الحافز الضائع.
ثم تأتي استعارته التي لا أستطيع التوقف عن التفكير فيها. تخيّل عاملًا لا يكلّ أمام كتلة من الرخام. بل يستطيع حتى السفر عبر الزمن: ينحت الرخام خطأً، فيُرجع الزمن، ويحاول من جديد، إلى الأبد. أدواته بدائية — لا يستطيع النحت كمايكل أنجلو، بل يكتفي برمي الحجارة — لكنه لا يتوقف أبدًا ولا يتعب أبدًا. وإذا مُنح محاولات كافية، فسيصل إلى نتيجة مذهلة.
إلا إذا كان أعمى.
فإن لم يستطع العامل أن يرى الرخام، فلن تنفعه أي مثابرة ولا أي سفر عبر الزمن. محاولاته لا تستنير بنتائج ما سبقها. إنه يرمي الحجارة في الظلام فحسب.
هذا هو وكيل البرمجة لديك من دون حلقات تغذية راجعة. ولهذا توقفت عن تحسين أوامري النصية وبدأت أحسّن حواسّ وكيلي.
نوعان من البصر
يحتاج وكيل البرمجة إلى رؤية شيئين مختلفين:
- ما فعله الكود فعلًا — سلوك الإنتاج: الأخطاء، والسجلات، والخطوط الزمنية، والطلب الذي فشل في الساعة 11:51 وكل ما حدث حوله.
- ما سيفعله الكود — عواقب التغيير الذي أجراه للتو، قبل أن يُنشر: هل ما زال التدفق يعمل؟ هل انتهت قاعدة البيانات إلى الحالة الصحيحة؟ هل استدعينا واجهة برمجة التطبيقات الخارجية بالطريقة التي نظنها؟
على Cloudflare، أصبح كلا الأمرين اليوم شيئًا يستطيع الوكيل تشغيله بنفسه، من دون أن أنقر عبر لوحات التحكم أو أجالس بيئة staging. الأول يأتي من مهارات Cloudflare وخوادم MCP؛ والثاني من @cloudflare/vitest-pool-workers وبنية اختبارات صُممت عمدًا على مبدأ المحلي أولًا.
دعني أعرض عليك الاثنين، بمواد حقيقية (مع إخفاء طفيف للهوية) من مشروعي: منصة متعددة المستأجرين على Workers تتكامل مع منصات تداول العملات المشفرة — واجهة Hono API، وDurable Objects مع SQLite، وR2، وD1، وdrizzle-orm، وكل ما يلزم.
الجزء الأول: دَع الوكيل يقرأ الإنتاج
الإعداد
توفّر Cloudflare مهارات رسمية لـ Claude Code — وحدات إرشاد سياقية لـ Workers وDurable Objects وwrangler وAgents SDK وغيرها. وهي تتبع فلسفة الاسترجاع أولًا: بدلًا من الوثوق بما حفظه النموذج عن المنصة في عام 2024، تطلب منه المهارة أن يذهب ويبحث عن المعلومات بنفسه.
ثم يأتي الجزء الذي منح وكيلي عيونًا حقيقية على الإنتاج: خادم Workers Observability MCP. أمر واحد:
claude mcp add cloudflare-observability --transport http https://observability.mcp.cloudflare.com/mcpصادِق على الاتصال عبر /mcp (فهو يشغّل تدفق OAuth الخاص به مقابل حسابك على Cloudflare)، وسيصبح بمقدور وكيلك الآن الاستعلام عن كل سطر سجلّ أصدرته خدمات Workers لديك خلال الأيام السبعة الماضية: مرشّحات، وبحث نصي كامل، وتجميعات group-by، وحسابات النسب المئوية. ليس wrangler tail مع الأمل بأن يتكرر الخطأ — بل قياس عن بُعد تاريخي من الإنتاج، قابل للاستعلام في صورة منظّمة.
قصة من ميدان المعركة
إليك ما أقنعني. بدأ تدفق OAuth لدينا الخاص بربط حساب المستخدم على منصة التداول يفشل في الإنتاج مع الرسالة:
OAuth completion failed: <Exchange> API error: Temporary lockoutاستجابة HTTP 400، والاتصال يُبلَّغ عنه كفاشل. إلا أن… مفتاح API أُنشئ فعلًا على منصة التداول، وبالصلاحيات الصحيحة. وكان المستخدم يراه في حسابه. شيء ما كان يدّعي الفشل رغم النجاح.
في السابق كنت سأمضي المساء في لوحة التحكم: تصفية حسب URL، وتحديق في الطوابع الزمنية، وفتح خمسة عشر إدخال سجلّ، وربط الأحداث يدويًا. بدلًا من ذلك، لصقت سطر سجلّ واحدًا كعيّنة في Claude Code وطلبت منه التحقيق.
ما فعله، بشكل مستقل تمامًا، كان هو الجزء المثير:
أولًا، قرأ الكود قبل أن يلمس السجلات. تتبّع موضع رمي الاستثناء بدقة: دالة الاستدعاء الراجع لدينا كانت تنشئ مفتاح API، ثم تستدعي فورًا نقطة نهاية الرصيد الخاصة في منصة التداول كخطوة “تحقّق” — وتعامل أي خطأ على أنه قاتل. المحفظة لم تُخزَّن قط. المفتاح كان موجودًا على منصة التداول؛ نحن ببساطة رميناه وأخبرنا المستخدم أن العملية فشلت.
ثم توجّه إلى السجلات لاختبار الفرضية. كان في سطر السجلّ الذي أعطيته معرّف من نوع ULID. فكّ الوكيل ترميز الطابع الزمني منه (معرّفات ULID تتضمن المللي ثانية — لم أكن أعلم ذلك بصراحة)، فحصل على لحظة الفشل بالضبط، واستعلم عن نافذة زمنية حولها:
{ "view": "events", "timeframe": { "from": "…T10:30:00Z", "to": "…T12:10:00Z" }, "parameters": { "filters": [ { "key": "$metadata.service", "operation": "eq", "value": "workers-prod" }, { "key": "$metadata.level", "operation": "eq", "value": "error" } ], "needle": { "value": "lockout" } }}ثم وسّع نطاق النظر وجمّع النتائج. بدلًا من التحديق في أحداث منفردة، أجرى عدًّا مجمَّعًا حسب $metadata.trigger عبر الأسبوع كله. وكانت النتيجة هي الدليل القاطع: خطأ “Temporary lockout” لم يكن مشكلة OAuth على الإطلاق. لقد ظهر في أربعة أنظمة فرعية لا علاقة بينها — نقطة نهاية تحديث الأرصدة، ونقطة نهاية عناوين الإيداع، ومهمة cron، ومنبّه Durable Object يقوم باستطلاع عمليات السحب. كانت حالة تقييد على مستوى الحساب من جهة منصة التداول، موجودة مسبقًا قبل أن يعمل استدعاء OAuth الراجع أصلًا. مفتاح API جديد تمامًا وصالح تمامًا دخل غرفة موصدة.
الخط الزمني المُعاد بناؤه بدا كلوحة محقق جنائي:
11:39 burst of "Invalid key" errors (a stored wallet with a dead key, hammered by balance refresh)11:45 cron job hits "Temporary lockout" ← account already locked, before any OAuth11:51 OAuth connect: key created OK → balance verification → "Temporary lockout" → 40011:56 user retries → 500 "Missing idempotency key" ← a *second*, unrelated bug11:57 user retries → 50011:57 user retries → 500وفي أثناء ذلك، وجد خطأين إضافيين لم أسأل عنهما: مسار إعادة المحاولة كان يعيد 500 لأن ملف تعريف ارتباط كان مفقودًا ومعالج الأخطاء لم يغطِّ تلك الحالة (فلم تصل إلى الواجهة حتى رسالة فشل)، ومهمة cron بتوقيت * * * * * كانت تُغرق السجلات بمئات التحذيرات غير الضارة في الدقيقة — وهو أمر بات أهم مما كان، لأن ضجيج السجلات يفسد الآن استعلامات الوكيل أيضًا، لا استعلاماتي وحدي.
أما السبب الجذري النهائي فتبيّن أنه أطرف: منصة التداول تطبّق فترة تهدئة أمنية تقارب 15 دقيقة على استدعاءات API الخاصة كلما اتصل حساب من جهاز أو عنوان IP جديد — وهذا بالضبط ما يكون عليه اتصال OAuth. تصميمنا القائم على التحقق المتزامن مباشرة بعد الإنشاء كان محكومًا عليه بنيويًا بالفشل عند أول اتصال. لم يكن الحل منطق إعادة المحاولة؛ بل تخزين المفتاح فورًا وتأجيل فحص الرصيد إلى ما بعد فترة التهدئة.
لم أفتح لوحة تحكم Cloudflare قط. الوكيل صاغ فرضيات انطلاقًا من الكود، واختبرها مقابل بيانات القياس عن بُعد من الإنتاج، ثم راجعها. هذا هو نحّات antirez الذي لا يكلّ — لكن بعيون.
عثرات من قلب التجربة
ثلاثة أمور ستلدغك، فلتقرأها كي لا تفعل:
خوادم MCP المضافة في منتصف الجلسة تحتاج إلى إعادة اتصال. الأمر claude mcp add يحدّث ملف الإعدادات، لكن جلسة Claude Code الجارية لن ترى أدوات الخادم الجديد حتى تنفّذ /mcp في تلك الجلسة (أو تعيد تشغيلها). خسرت عشر دقائق من الحيرة بسبب هذا.
نظافة السجلات أصبحت الآن جزءًا من أداء الوكيل. البحث عن إبرة في خدمة صاخبة يعيد لك الضجيج نفسه. أول استعلام لي من نوع “أرني كل ما حدث حول لحظة الفشل” عاد وكله بنسبة 100% تحذيرات cron. إن أردت للوكلاء أن يصحّحوا الأخطاء انطلاقًا من سجلاتك، فعامل السخام في السجلات كخلل له كلفة حقيقية.
الجزء الثاني: نهج TDD المحلي أولًا هو عين الوكيل الأخرى
بصر الإنتاج يخبرك بما حدث من خطأ. أما الحلقة الثانية — تلك التي تجعل الوكيل منتِجًا لا مجرد مشخِّص — فهي حزمة اختبارات يستطيع تشغيلها بنفسه، وتجيبه بصدق، في ثوانٍ.
مفتاح الحل على Cloudflare هو @cloudflare/vitest-pool-workers: اختباراتك لا تعمل في Node مع واجهات منصة مزيّفة — بل تعمل داخل workerd، بيئة تشغيل Workers الفعلية، التي يُقلعها Miniflare من ملف wrangler.jsonc الحقيقي لديك. Durable Objects، وتخزين SQLite الخاص بها، وR2، وD1، وKV، ومحدِّدات المعدّل: كلها تطبيقات حقيقية، وكلها محلية، وكلها ضمن العملية نفسها.
export default defineWorkersConfig({ test: { sequence: { concurrent: false }, poolOptions: { workers: { isolatedStorage: false, wrangler: { configPath: './wrangler.jsonc' }, // ← the whole platform, in-process moduleRules: [{ type: 'Text', include: ['**/*.sql'] }], }, }, },})إليك ما يتيحه ذلك عمليًا في قاعدة الكود لدي.
قاعدة البيانات في اختباراتك هي قاعدة بيانات الإنتاج
كل مستأجر في نظامي هو Durable Object تُدار قاعدة SQLite الخاصة بـ ctx.storage لديه بواسطة drizzle-orm. الترحيلات تعمل في مُنشئ الـ DO:
import { drizzle } from 'drizzle-orm/durable-sqlite';import { migrate } from 'drizzle-orm/durable-sqlite/migrator';import migrations from '../generated-migrations';
constructor(ctx: DurableObjectState, env: Env) { this.db = drizzle(ctx.storage, { schema: tenantSchema }); ctx.blockConcurrencyWhile(() => migrate(this.db, migrations));}ولأن vitest يُقلع صنف الـ DO نفسه تحت Miniflare، فإن قاعدة بيانات الاختبار المحلية تملك مخطط الإنتاج بالضبط — الترحيلات نفسها، والمحرك نفسه، ولا وجود لشيء من قبيل “محاكاة بنكهة SQLite لقاعدة Postgres لدينا”. (تعقيدة واحدة: صندوق Workers المعزول لا يستطيع قراءة الملفات من القرص، لذا تقوم خطوة بناء صغيرة بتوليد ملفات الترحيل .sql كوحدة JS نصية قبل تشغيل الحزمة. قبيح، لكنه فعّال.)
تأكيدات الصندوق الأبيض مع runInDurableObject
تكشف cloudflare:test عن باب هروب سحري: الوصول إلى داخل نسخة Durable Object وتشغيل تأكيدات على حالتها الخاصة.
const identities = await runInDurableObject(orgDb, async (instance: TenantDurableObject) => { const db = (instance as any).db; return db.select().from(cexIdentities).all();});expect(identities).toHaveLength(0);هذا هو الفرق بين “أعادت نقطة النهاية 200” و”الصف وصل فعلًا إلى قاعدة البيانات، والسر مشفَّر أثناء التخزين”. حزمتي تستخدم هذا في 46 ملف اختبار.
واجهات برمجة التطبيقات الخارجية تتحول إلى تأكيدات صارمة
الجزء الأكثر إخافة في أي تكامل مع منصة تداول هو الاستدعاءات الصادرة — وهو الجزء الذي يعشق الوكلاء اختلاقه أكثر من غيره. أداة fetchMock من cloudflare:test تحوّل ذلك إلى عقد:
beforeEach(() => { fetchMock.activate(); fetchMock.disableNetConnect(); // any unmocked outbound call = test failure});
fetchMock.get('https://api.exchange.example') .intercept({ method: 'POST', path: '/oauth/token' }) .reply(200, oauthTokenSuccessFixture);
// …run the flow…
fetchMock.assertNoPendingInterceptors(); // every expected call actually happenedاستدعاء disableNetConnect() يعني أن الوكيل لا يستطيع بالخطأ أن يختبر ضد الإنترنت الحقيقي، وأن أي استدعاء API إضافي مُختلَق سيفشل بصوت عالٍ بدلًا من أن يعمل بصمت “على نحو مقبول تقريبًا”. أما assertNoPendingInterceptors() فتعني أن الاستدعاء المفقود سيفشل هو الآخر. المحاكاة هنا ليست بديلًا صوريًا؛ إنها مواصفة.
الحلقة الذهبية
بجمع كل ذلك، يمارس اختبار واحد المسار الرأسي بأكمله: محاكاة نقاط النهاية الثلاث لمنصة التداول ← استدعاء مسار Hono الحقيقي ← التأكد من استجابة HTTP، وعقد المحاكاة، و حالة SQLite داخل Durable Object:
it('completes OAuth → API key → balance → wallet storage', async () => { fetchMock.get(EXCHANGE).intercept({ path: '/oauth/token', method: 'POST' }).reply(200, tokenFixture); fetchMock.get(EXCHANGE).intercept({ path: '/oauth/api-key', method: 'POST' }).reply(200, keyFixture); fetchMock.get(EXCHANGE).intercept({ path: '/private/Balance', method: 'POST' }).reply(200, balanceFixture);
const response = await app.request(callbackUrl, { headers }, env);
expect(response.status).toBe(200); fetchMock.assertNoPendingInterceptors();
const wallet = await runInDurableObject(orgDb, (i: TenantDurableObject) => i.getWallet('wallet-123')); expect(wallet).toMatchObject({ exchange: 'exchange', type: 'long-living' }); expect(wallet!.apiSecret).not.toBe(keyFixture.result.secret); // encrypted at rest});لماذا يهمّ هذا الوكلاءَ على وجه التحديد؟ عُد إلى النحّات:
- السرعة تغذّي المثابرة. الأمر
npx vitest run test/oauth2/callback.test.tsيمنح الوكيل حكمًا بالأحمر أو الأخضر على المكدّس الكامل في ثوانٍ. كل حجر يُرمى يُقيَّم فورًا. خمسون تكرارًا تكلّف دقائق لا أيامًا. - الحتمية تُبقي التغذية الراجعة صادقة. لا بيئة staging متقلّبة، ولا انحراف في بيئة مشتركة، ولا “كانت تعمل على جهازي”. حالة Miniflare تُمسح في بداية كل تشغيل.
- الصرامة تصطاد الاختلاقات. الجمع بين
disableNetConnectوassertNoPendingInterceptorsهو جهاز مضاد للهلوسة: الوكيل لا يستطيع اختراع تفاعل API “موجود على الأرجح” — فالعقد قابل للتنفيذ. - إنها خدمة ذاتية. الوكيل لا يطلب مني النقر عبر واجهة رسومية للتحقق. إنه يكتب الاختبار الفاشل، ويجعله ينجح، ويريني الناتج. لطالما كان TDD انضباطَ حلقةِ تغذيةٍ راجعة؛ الوكلاء ببساطة هم أول المطوّرين الذين يملكون من المثابرة ما يكفي لاستغلاله استغلالًا كاملًا.
(ملاحظة صريحة: تبنّي المحلي أولًا إلى هذا الحد على منصة فتيّة له تكاليفه. أنا حاليًا أشحن معتمدًا على تشعّبات مجتمعية مرقَّعة من drizzle-orm وbetter-auth لجعل المحوّلات تتصرف كما ينبغي. ضريبة المتبنّي المبكر.)
المئة ضعف بصدق
عبارة “100x” ادعاء كبير، فدعني أحدد موضعها بدقة. ليست المسألة سرعة الكتابة على لوحة المفاتيح. إنها حاصل ضرب عدد التكرارات × صدق التغذية الراجعة، وتبدو هكذا:
| المهمة | أنا، يدويًا | وكيل بعيون |
|---|---|---|
| ”لماذا حدث خطأ 400 هذا في الإنتاج؟“ | 30–60 دقيقة من التنقيب في لوحة التحكم، إن حالفني الحظ | أمر نصي واحد؛ الوكيل يربط الكود بأسبوع كامل من السجلات، ويعيد خطًا زمنيًا وخطأين إضافيين |
| ”هل كسرت للتو تدفق السحب؟“ | النشر إلى staging، والنقر عبر الواجهة | vitest run — حكم على المكدّس الكامل في ثوانٍ، بما في ذلك حالة الـ DO |
| ”هل نستدعي واجهة API الخاصة بمنصة التداول بشكل صحيح؟“ | قراءة وثائقهم مرة أخرى، والأمل | assertNoPendingInterceptors() — العقد هو اختبار |
| ”هل ما زالت واجهة API هذه للمنصة بالشكل الذي أتذكره؟“ | التبديل إلى تبويب الوثائق | مهارة Cloudflare تسترجع الوثائق الحالية بدلًا من الوثوق ببيانات التدريب |
كان الوكيل دائمًا مثابرًا. وكان دائمًا سريعًا. لم يكن هذان يومًا هما عنق الزجاجة — البصر كان كذلك. اربط له قياسًا عن بُعد من الإنتاج يمكنه الاستعلام عنه، وعالمًا محليًا يمكنه محاكاته، وحينها سيرى العامل الذي لا يكلّ أمام الرخام أخيرًا أين يسقط كل حجر يرميه.
الآن، إنه ينحت.
الشكر والروابط
- سلفاتوري سانفيليبو (antirez)، Il trucco decisivo (davvero) per lavorare coi coding agent — تأطير النحّات الأعمى الذي ألهم هذه التدوينة. Grazie.
- cloudflare/skills — مهارات Agent الرسمية لـ Claude Code وغيره من الوكلاء.
- دليل إعداد وكلاء Cloudflare لـ Claude Code — المهارات وخوادم MCP، بما فيها خادم Observability MCP.
- تكامل Vitest مع Workers —
@cloudflare/vitest-pool-workersوrunInDurableObjectوfetchMock.