Skip to content

Latest commit

 

History

History
928 lines (690 loc) · 61.9 KB

EMBEDDED-SERVICES.md

File metadata and controls

928 lines (690 loc) · 61.9 KB

Embedded Services (العربية)

🌐 Languages: 🇺🇸 English · 🇪🇹 am · 🇦🇿 az · 🇧🇬 bg · 🇧🇩 bn · 🇧🇦 bs · 🇨🇿 cs · 🇩🇰 da · 🇩🇪 de · 🇬🇷 el · 🇪🇸 es · 🇪🇪 et · 🇮🇷 fa · 🇫🇮 fi · 🇫🇷 fr · 🇮🇪 ga · 🇮🇳 gu · 🇳🇬 ha · 🇮🇱 he · 🇮🇳 hi · 🇭🇷 hr · 🇭🇺 hu · 🇦🇲 hy · 🇮🇩 id · 🇳🇬 ig · 🇮🇹 it · 🇯🇵 ja · 🇬🇪 ka · 🇰🇭 km · 🇮🇳 kn · 🇰🇷 ko · 🇱🇹 lt · 🇱🇻 lv · 🇮🇳 ml · 🇮🇳 mr · 🇲🇾 ms · 🇲🇹 mt · 🇲🇲 my · 🇳🇵 ne · 🇳🇱 nl · 🇳🇴 no · 🇮🇳 or · 🇮🇳 pa · 🇵🇭 phi · 🇵🇱 pl · 🇵🇹 pt · 🇧🇷 pt-BR · 🇷🇴 ro · 🇷🇺 ru · 🇱🇰 si · 🇸🇰 sk · 🇸🇮 sl · 🇷🇸 sr · 🇸🇪 sv · 🇰🇪 sw · 🇮🇳 ta · 🇮🇳 te · 🇹🇭 th · 🇹🇷 tr · 🇺🇦 uk-UA · 🇵🇰 ur · 🇺🇿 uz · 🇻🇳 vi · 🇳🇬 yo · 🇨🇳 zh-CN · 🇹🇼 zh-TW


الإصدار: v3.8.44 آخر تحديث: 2026-09-09 الجمهور المستهدف: المهندسون الذين يضيفون الخدمات المضمّنة أو يصونونها أو يصححون أخطاءها (9Router، CLIProxyAPI، Mux، Bifrost، open-wa).

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


جدول المحتويات

  1. نظرة عامة
  2. البنية — 4 طبقات
  3. آلة حالات دورة الحياة
  4. مرجع API
  5. الأمان
  6. إضافة خدمة مضمّنة جديدة
  7. استكشاف الأخطاء وإصلاحها
  8. الأسئلة الشائعة

1. نظرة عامة

لماذا الخدمات المضمّنة؟

تم تضمين ست خدمات:

الخدمة حزمة npm المنفذ الافتراضي الغرض
9Router 9router 20130 موجّه ذكاء اصطناعي يمكن لـ OmniRoute استخدامه كمزوّد فرعي. تُعرض النماذج بصيغة 9router/{sub}/{model}
CLIProxyAPI ملف ثنائي من إصدار GitHub (cliproxy) 8317 محوّل وكيل محلي لتدفقات مصادقة Anthropic CLI. يوفّر توجيهًا احتياطيًا عند انتهاء صلاحية رموز OAuth
Mux mux (تشغيل mux server دون واجهة رسومية) 8322 برنامج خفي محلي لتنسيق الوكلاء (coder/mux). تُدار دورة حياته فقط — وليس هدفًا للتوجيه (لا ينفّذ تمريرًا وكيلاً لطلبات LLM).
Bifrost @maximhq/bifrost 8080 واجهة خلفية لترحيل بوابة ذكاء اصطناعي مكتوبة بلغة Go. عند تشغيلها، يجري اختيارها تلقائيًا بواسطة مسار الترحيل (/v1/relay/)
Dario @askalf/dario 3456 وكيل لاشتراك Claude — بديل/خيار تجاوز فشل لـ CLIProxyAPI لحركة البيانات المتوافقة مع Claude Code؛ يصبح المفتاح المحقون DARIO_ADMIN_TOKEN الذي يحمي مستوى تحكم OAuth ضمن /admin/*
open-wa @open-wa/wa-automate 8323 أتمتة WhatsApp Web (متصفح Chromium دون واجهة رسومية عبر Puppeteer). تُدار دورة حياته فقط — وليس هدفًا للتوجيه.

تتبع الخدمات الست جميعها نموذج الإشراف نفسه:

  • يثبّتها OmniRoute ضمن DATA_DIR/services/{name}/ (بمعزل عن ملف package.json الخاص بـ OmniRoute)
  • يشغّلها OmniRoute ويراقبها كعمليات فرعية
  • يحقن OmniRoute مفتاح API مؤقتًا في بيئة العملية الفرعية ويدوّره دون توقف الخدمة (حيثما ينطبق)
  • جميع مسارات الإدارة (/api/services/*) هي LOCAL_ONLY — ولا يمكن الوصول إليها إلا من عنوان الاسترجاع المحلي (القاعدة الصارمة رقم 17)

القرارات الرئيسية (من خطة التصميم)

القرار القيمة
وصول لوحة المعلومات إلى واجهة 9Router الأصلية وكيل عكسي عند /dashboard/providers/services/9router/embed/*
آلية التثبيت npm install {package} عبر execFile (دون استيفاء أوامر الصدفة)
وضع الاستخدام تسجيل المزوّد بصيغة 9router/{sub}/{model} في محرك التوجيه
إدارة مفتاح API ينشئ OmniRoute المفتاح ويشفّره عند التخزين (AES-256-GCM) ويحقنه عبر متغيرات البيئة
موقع لوحة المعلومات /dashboard/providers/services (ثلاث علامات تبويب)
التشغيل التلقائي مفتاح تبديل لكل خدمة، ويكون معطّلًا افتراضيًا

2. البنية المعمارية — 4 طبقات

┌────────────────────────────────────────────────────────────────────┐
│  الطبقة 1 — واجهة المستخدم                                        │
│  /dashboard/providers/services  (علامات التبويب: CLIProxyAPI | 9Router | Mux)│
│  السجلات المباشرة (SSE)، بدء/إيقاف/إعادة تشغيل/تحديث، الإعدادات، التثبيت│
│                                                                    │
│  src/app/(dashboard)/dashboard/providers/services/                 │
│    ├── page.tsx               الغلاف + توجيه علامات التبويب عبر ?tab=│
│    ├── tabs/                  CliproxyServiceTab, NinerouterServiceTab,│
│    │                          MuxServiceTab                        │
│    └── components/            ServiceStatusCard, ServiceLifecycleButtons,│
│                               ServiceLogsPanel, ApiKeyCard, ...    │
└──────────────────────┬─────────────────────────────────────────────┘
                       │ HTTP (طلب Next.js عبر fetch)
┌──────────────────────▼─────────────────────────────────────────────┐
│  الطبقة 2 — واجهة API (LOCAL_ONLY — عبر loopback فقط)              │
│                                                                    │
│  /api/services/9router/{install|start|stop|restart|update|         │
│                          rotate-key|status|auto-start|logs}        │
│  /api/services/cliproxy/{install|start|stop|restart|update|        │
│                           status|auto-start|logs}                  │
│  /api/services/mux/{install|start|stop|restart|update|             │
│                      status|auto-start|logs}                       │
│  /dashboard/providers/services/9router/embed/[...path]             │
│    (وكيل عكسي لـ HTTP وWebSocket ← المنبع 9Router)                 │
│                                                                    │
│  البوابة: تتضمن LOCAL_ONLY_API_PREFIXES المسار "/api/services/" و  │
│        "/dashboard/providers/services/*/embed/"                    │
└──────────────────────┬─────────────────────────────────────────────┘
                       │ استدعاءات داخل العملية
┌──────────────────────▼─────────────────────────────────────────────┐
│  الطبقة 3 — ServiceSupervisor ‏(src/lib/services/)                  │
│                                                                    │
│  ServiceSupervisor.ts   مشرف عام (child_process.spawn)             │
│    ├── التثبيت:    execFile('npm', ['install', pkg, '--prefix'])    │
│    ├── البدء:      spawn(node, [entrypoint], {env, cwd})           │
│    ├── مفتاح API:  crypto.randomBytes(32) ← env NINEROUTER_API_KEY  │
│    ├── المنفذ:     20130 لـ 9Router (قابل للتهيئة)                 │
│    ├── السجلات:    مخزن حلقي لـ stdio بحجم 5 MB ← أحداث SSE        │
│    ├── الصحة:      HTTP GET /health كل 2–5 ثوانٍ، استرداد كسول    │
│    └── دورة الحياة: SIGTERM لمدة 15 ثانية ← SIGKILL               │
│                                                                    │
│  registry.ts        getSupervisor(name) / registerSupervisor()     │
│  bootstrap.ts       يهيئ جميع SERVICES[] عند بدء العملية           │
│  apiKey.ts          getOrCreateApiKey(), generateServiceApiKey()   │
│  modelSync.ts       طلب GET /v1/models دوري ← جدول service_models │
│  ringBuffer.ts      مخزن سجلات دائري (5 MB لكل خدمة)               │
│  healthCheck.ts     مسبار دوري لفحص صحة HTTP                       │
│  installers/        ninerouter.ts, cliproxy.ts, mux.ts, openwa.ts  │
│                      (مهايئات التثبيت)                             │
└──────────────────────┬─────────────────────────────────────────────┘
                       │ HTTP متوافق مع OpenAI ‏(loopback)
┌──────────────────────▼─────────────────────────────────────────────┐
│  الطبقة 4 — المزوّد / التوجيه                                     │
│                                                                    │
│  open-sse/executors/ninerouter.ts                                  │
│    يعيد البحث عن المنفذ ومفتاح API لكل طلب (من دون تخزين مؤقت).   │
│    يزيل البادئة "9router/" من معرّف النموذج قبل تمريره عبر الوكيل. │
│    يعيد 503 service_not_running إذا لم يكن المشرف في حالة "running".│
│                                                                    │
│  src/shared/constants/providers.ts                                 │
│    إدخال لـ "9router":‏ isEmbeddedService: true                    │
│                                                                    │
│  open-sse/config/providerRegistry.ts                               │
│    تُخزّن النماذج بصيغة "9router/{sub}/{model}" (ببادئة).          │
│    تتم مزامنتها كل 5 دقائق بواسطة modelSync.ts.                    │
│                                                                    │
│  تتم إدارة دورة حياة Mux فقط (الطبقات 1-3) — فهو برنامج خفي       │
│  لتنسيق الوكلاء، وليس وكيل LLM، لذلك لا يملك منفّذًا أو إدخال      │
│  مزوّد في الطبقة 4، ولا يُستخدم مطلقًا هدفًا للتوجيه.              │
└────────────────────────────────────────────────────────────────────┘

ملفات المصدر الرئيسية

الملف الدور
src/lib/services/ServiceSupervisor.ts الفئة الأساسية: دورة الحياة، القفل، الصحة، المخزن الحلقي
src/lib/services/bootstrap.ts التسجيل على مستوى العملية والبدء التلقائي
src/lib/services/registry.ts خريطة مفردة tool → supervisor
src/lib/services/apiKey.ts إنشاء المفاتيح وتشفير AES-256-GCM للبيانات المخزنة
src/lib/services/modelSync.ts مزامنة دورية للنماذج (5 دقائق) + عند الطلب
src/lib/services/ringBuffer.ts مخزن دائري للسجلات بسعة 5 MB مع الاشتراك عبر SSE
src/lib/services/healthCheck.ts فحص صحة HTTP (بفاصل زمني قابل للتهيئة)
src/lib/services/installers/ninerouter.ts تثبيت 9Router وتحديثه وإلغاء تثبيته باستخدام npm
src/lib/services/installers/cliproxy.ts تثبيت CLIProxyAPI وتحديثه وإلغاء تثبيته باستخدام npm
src/lib/services/installers/mux.ts تثبيت Mux وتحديثه وإلغاء تثبيته باستخدام npm
src/lib/services/installers/openwa.ts تثبيت open-wa وتحديثه وإلغاء تثبيته باستخدام npm
src/app/api/services/9router/_lib.ts الدالة المساعدة getOrInitSupervisor()
src/app/api/services/[name]/logs/route.ts نقطة نهاية مشتركة لسجلات SSE
open-sse/executors/ninerouter.ts منفّذ المزوّد (الطبقة 4)

3. آلة حالات دورة الحياة

                    install()
  ┌─────────────┐ ──────────► ┌─────────────┐
  │ not_installed│             │   stopped   │◄──────────────────┐
  └─────────────┘             └──────┬──────┘                   │
                                     │ start()                   │
                                     ▼                           │ stop()
                               ┌──────────┐                      │
                               │ starting │                      │
                               └────┬─────┘                     │
                  نجاح فحص الصحة    │         تعطّل / SIGTERM    │
                               ┌────▼─────┐  (خروج خلال 5 ث)     │
                               │ running  │──── تعطّل ──────────►┤
                               └────┬─────┘                   ┌─▼────┐
                             stop() │                          │error │
                                    ▼                          └──────┘
                               ┌──────────┐
                               │ stopping │
                               └──────────┘

تُخزَّن الحالات في جدول قاعدة البيانات version_manager (العمود status) وتُعكس في حالة ServiceSupervisor الموجودة في الذاكرة. تكون الحالة الموجودة في الذاكرة هي المرجع المعتمد للعملية قيد التشغيل؛ أما حالة قاعدة البيانات فهي البديل الدائم عند الإقلاع.

انتقالات الحالة

من الحدث إلى
not_installed نجاح install() stopped
stopped استدعاء start() starting
starting يُرجع فحص الصحة 200 running
starting خروج العملية قبل أن تصبح سليمة error
running استدعاء stop() stopping → stopped
running خروج العملية بشكل غير متوقع (< 5 ث) error (تعطّل سريع)
running خروج العملية بشكل غير متوقع (> 5 ث) error
error استدعاء start() starting
أي حالة stop() أثناء stopping بلا إجراء

قفل العمليات

ينفّذ ServiceSupervisor عمليات دورة الحياة تسلسليًا من خلال قفل عمليات غير متزامن (withLock()). تؤدي استدعاءات start() المتزامنة على المشرف نفسه إلى إنشاء عملية واحدة فقط؛ إذ ينتظر المستدعي الثاني ويُرجع الحالة الحالية. يمنع ذلك حالات التسابق عندما يحدث، على سبيل المثال، التشغيل التلقائي والضغط على زر في واجهة المستخدم في الوقت نفسه.


4. مرجع API

جميع المسارات ضمن /api/services/ هي LOCAL_ONLY (واجهة الاسترجاع فقط، القاعدة الصارمة رقم 17). تتلقى الطلبات الواردة من خارج واجهة الاسترجاع 403 LOCAL_ONLY بغض النظر عن رمز المصادقة.

4.1 نقاط نهاية 9Router (11 مسارًا)

POST /api/services/9router/install

تثبيت 9Router من npm. ينشئ DATA_DIR/services/9router/ مع ملف package.json ومجلد node_modules/ خاصين به. لا يتعارض مع تبعيات OmniRoute الخاصة.

نص الطلب (جميع الحقول اختيارية):

{ "version": "latest" }
الحقل النوع القيمة الافتراضية الوصف
version string "latest" وسم إصدار npm أو semver المراد تثبيته

الاستجابات:

الحالة الوصف
200 { ok: true, installedVersion: "x.y.z", path: "..." }
400 نص طلب غير صالح (فشل تحقق Zod)
409 التثبيت جارٍ بالفعل (القفل محجوز)
500 فشل تثبيت npm — راجع message لمعرفة الخطأ بصيغة واضحة

ملاحظات: يستخدم execFile('npm', [...]) — بلا shell وبلا استيفاء (القاعدة الصارمة رقم 13). تُعرض أخطاء EACCES في صورة رسائل واضحة.


POST /api/services/9router/start

بدء تشغيل 9Router. يسجّل مشرفًا إذا لم يكن مسجلًا بالفعل، ثم يستدعي supervisor.start(). العملية متكررة آمنة إذا كانت الخدمة قيد التشغيل بالفعل.

نص الطلب: لا يوجد

الاستجابات:

الحالة الوصف
200 كائن ServiceStatus (راجع المخطط أدناه)
409 9Router غير مثبت (status: "not_installed")
503 فشل بدء التشغيل (خطأ في العملية — راجع lastError)

مخطط ServiceStatus:

{
  "tool": "9router",
  "state": "running",
  "pid": 12345,
  "port": 20130,
  "health": "healthy",
  "startedAt": "2026-05-25T10:00:00.000Z",
  "lastError": null
}

POST /api/services/9router/stop

إيقاف 9Router بأمان. يرسل SIGTERM، وينتظر 15 ثانية، ثم يرسل SIGKILL إذا ظلت العملية قيد التشغيل. العملية متكررة آمنة إذا كانت الخدمة متوقفة بالفعل.

نص الطلب: لا يوجد

الاستجابات:

الحالة الوصف
200 ServiceStatus (الحالة: "stopped")
503 فشل الإيقاف على نحو غير متوقع

POST /api/services/9router/restart

يكافئ تنفيذ stop() ثم start() ضمن قفل العملية.

نص الطلب: لا يوجد

الاستجابات: مماثلة لـ start (تعيد كائن ServiceStatus النهائي).


POST /api/services/9router/update

يحدّث 9Router إلى إصدار npm أحدث. إذا كانت الخدمة قيد التشغيل، فتُوقَف أولًا، ثم يُشغَّل تثبيت npm (لتثبيت الإصدار الأحدث في الموضع نفسه)، وبعد ذلك تُعاد عملية تشغيل الخدمة.

نص الطلب (جميع الحقول اختيارية):

{ "version": "latest" }

الاستجابات:

الحالة الوصف
200 { ok: true, previousVersion: "...", installedVersion: "..." }
400 نص غير صالح
500 فشل تحديث npm

POST /api/services/9router/rotate-key

ينشئ مفتاح API جديدًا لـ 9Router، ويشفّره عند التخزين، ويعيد تشغيل الخدمة (إذا كانت قيد التشغيل) لكي تستخدم المفتاح الجديد من بيئتها. يُلغى المفتاح القديم فورًا.

نص الطلب: لا يوجد

الاستجابات:

الحالة الوصف
200 { keyRotated: true, restarted: boolean }
500 فشل تدوير المفتاح

الأمان: لا يُعاد المفتاح الجديد مطلقًا في الاستجابة (لمنع تسريب بيانات الاعتماد). ويُخزَّن مشفرًا (AES-256-GCM) في جدول version_manager.


GET /api/services/9router/status

يعيد الحالة المجمعة المباشرة ومن قاعدة البيانات، بما يشمل بيانات تعريف الإصدار ومعاينة مفتاح API.

الاستجابات:

الحالة الوصف
200 راجع المخطط أدناه
500 فشلت قراءة الحالة

مخطط الاستجابة:

{
  "tool": "9router",
  "state": "running",
  "pid": 12345,
  "port": 20130,
  "health": "healthy",
  "startedAt": "2026-05-25T10:00:00.000Z",
  "lastError": null,
  "installedVersion": "1.2.3",
  "latestVersion": "1.2.4",
  "updateAvailable": true,
  "apiKeyMasked": "nr_****abcd",
  "autoStart": false,
  "providerExpose": false
}

POST /api/services/9router/auto-start

تبديل علامة البدء التلقائي. عندما تكون enabled: true، تبدأ الخدمة تلقائيًا في المرة التالية التي يبدأ فيها OmniRoute (إذا كانت الخدمة مثبتة).

نص الطلب:

{ "enabled": true }

الاستجابات:

الحالة الوصف
200 { autoStart: true }
400 نص غير صالح

GET /api/services/9router/logs

تدفق SSE للسجلات المباشرة من المخزن المؤقت الحلقي لـ stdout/stderr الخاص بـ 9Router.

معاملات الاستعلام:

المعامل النوع القيمة الافتراضية الوصف
tail integer 200 عدد أسطر السجل السابقة التي تُرسل أولًا (الحد الأقصى 1000)
filter string لا شيء مرشح سلسلة فرعية غير حساس لحالة الأحرف (بلا regex — آمن من ReDoS)

أحداث SSE:

الحدث البيانات الوصف
snapshot LogLine[] الجزء السابق الأولي من السجل
log LogLine سطر سجل مباشر
heartbeat {} إبقاء الاتصال نشطًا كل 15 ثانية

مخطط LogLine:

{
  "ts": 1716633600000,
  "stream": "stdout",
  "line": "[9router] Listening on :20130"
}

الاستجابات:

الحالة الوصف
200 text/event-stream
400 المعامل filter طويل جدًا (> 200 حرف)
404 لم يتم العثور على الخدمة (المشرف غير مسجّل)

4.2 نقاط نهاية CLIProxyAPI (10 مسارات)

يمتلك CLIProxyAPI بنية نقاط النهاية نفسها الخاصة بـ 9Router باستثناء rotate-key، بالإضافة إلى accounts وprovider-expose وauto-restart-adopted. ويتلقى الآن مفتاح API مخصصًا لمستوى البيانات يُحقن عند التشغيل (needsApiKey: true في bootstrap.ts، ويُستخدم لمزامنة النماذج)؛ ويتضمن status حقولًا أقل.

الطريقة المسار الوصف
POST /api/services/cliproxy/install تثبيت CLIProxyAPI من npm
POST /api/services/cliproxy/start بدء تشغيل CLIProxyAPI
POST /api/services/cliproxy/stop إيقاف CLIProxyAPI
POST /api/services/cliproxy/restart إعادة تشغيل CLIProxyAPI
POST /api/services/cliproxy/update التحديث إلى إصدار أحدث
GET /api/services/cliproxy/status الحالة المباشرة + حالة قاعدة البيانات (من دون apiKeyMasked)
POST /api/services/cliproxy/auto-start تبديل التشغيل التلقائي

تعمل نقطة النهاية المشتركة GET /api/services/{name}/logs (راجع §4.1) مع الخدمات الأربع جميعها باستخدام المقطع الديناميكي [name].


4.3 نقاط نهاية Mux (8 مسارات)

يمتلك Mux بنية نقاط النهاية نفسها الخاصة بـ CLIProxyAPI — لا يوجد مسار rotate-key ضمن واجهة API (يُنشأ رمز الحامل بالطريقة نفسها المتبعة في 9Router عبر getOrCreateApiKey("mux") ويُحقن من خلال متغير البيئة MUX_SERVER_AUTH_TOKEN، ولكن لا توجد حتى الآن نقطة نهاية مخصصة للتدوير). تتم إدارة دورة حياة Mux فقط: فعلى خلاف 9Router، لا يمتلك منفّذًا للطبقة 4 ولا يُسجّل أبدًا كمزوّد توجيه.

الطريقة المسار الوصف
POST /api/services/mux/install تثبيت Mux من npm (npm i mux)
POST /api/services/mux/start بدء تشغيل Mux (mux server)
POST /api/services/mux/stop إيقاف Mux
POST /api/services/mux/restart إعادة تشغيل Mux
POST /api/services/mux/update التحديث إلى إصدار npm أحدث
GET /api/services/mux/status الحالة المباشرة + حالة قاعدة البيانات
POST /api/services/mux/auto-start تبديل التشغيل التلقائي

4.4 نقاط نهاية Bifrost (8 مسارات)

Bifrost هو نظام خلفي لترحيل بوابة ذكاء اصطناعي مكتوب بلغة Go (@maximhq/bifrost). ويستخدم بنية نقاط النهاية نفسها الخاصة بـ CLIProxyAPI (من دون rotate-key — يدير Bifrost مفاتيح المزوّدين الخاصة به في config.json ضمن -app-dir).

الطريقة المسار الوصف
POST /api/services/bifrost/install تثبيت Bifrost من npm (@maximhq/bifrost)
POST /api/services/bifrost/start بدء تشغيل Bifrost على المنفذ 8080 (افتراضيًا)
POST /api/services/bifrost/stop إيقاف Bifrost
POST /api/services/bifrost/restart إعادة تشغيل Bifrost
POST /api/services/bifrost/update التحديث إلى إصدار أحدث
GET /api/services/bifrost/status الحالة المباشرة + حالة قاعدة البيانات
POST /api/services/bifrost/auto-start تبديل التشغيل التلقائي
GET /api/services/bifrost/logs ذيل سجل SSE (عبر المسار الديناميكي المشترك [name]/logs)

ربط التوجيه: عندما لا يكون BIFROST_BASE_URL معيّنًا وتكون نسخة Bifrost الخاضعة للإشراف قيد التشغيل، تستخدم getBifrostRoutingConfig() (في routingBackend.ts) تلقائيًا http://127.0.0.1:{port} بوصفه عنوان URL الأساسي للترحيل. تكون الأولوية دائمًا لمتغير البيئة BIFROST_BASE_URL الصريح.


4.5 نقاط نهاية Dario (12 مسارًا)

بنية دورة الحياة نفسها الخاصة بالخدمات الأخرى (install وstart وstop وrestart وupdate وstatus وauto-start وauto-restart-adopted)، بالإضافة إلى مستوى تحكم OAuth محمي برمز ضمن admin/: ‏admin/accounts وadmin/import-from-omniroute وadmin/login-start وadmin/login-complete (جميعها محمية بواسطة DARIO_ADMIN_TOKEN).

4.6 نقاط نهاية open-wa (7 مسارات)

يشغّل open-wa (@open-wa/wa-automate) نسخة Chromium بلا واجهة رسومية (عبر Puppeteer) لأتمتة WhatsApp Web. ويستخدم بنية نقاط النهاية نفسها الخاصة بـ Mux (لا يوجد مسار rotate-key حتى الآن). تتم إدارة دورة حياته فقط — فهو ليس هدفًا للتوجيه، ولا يملك منفّذًا للطبقة 4 أو إدخال مزوّد.

الطريقة المسار الوصف
POST /api/services/openwa/install تثبيت open-wa من npm (@open-wa/wa-automate)
POST /api/services/openwa/start تشغيل open-wa على المنفذ 8323 (افتراضيًا)
POST /api/services/openwa/stop إيقاف open-wa
POST /api/services/openwa/restart إعادة تشغيل open-wa
POST /api/services/openwa/update التحديث إلى إصدار أحدث
GET /api/services/openwa/status الحالة المباشرة + حالة قاعدة البيانات
POST /api/services/openwa/auto-start تبديل التشغيل التلقائي
GET /api/services/openwa/logs ذيل سجل SSE (عبر المسار الديناميكي المشترك [name]/logs)

مفتاح API: يُحقن باسم WA_KEY — ويحوّله تجاوز متغيرات البيئة العام في open-wa، الذي يستخدم البادئة WA_*، إلى خيار CLI‏ --key/-k (dist/cli/setup.js::envArgs()، جرى التحقق منه مقابل الحزمة المثبتة بالإصدار 4.76.0). تُضاف إليه البادئة ow_ عند إنشائه بواسطة generateServiceApiKey(). يقرأ open-wa المفتاح مجددًا من ترويسة HTTP باسم key/api_key (وليس Authorization: Bearer)؛ ويُعفى /api-docs* صراحةً من عملية التحقق (setupAuthenticationLayer في dist/cli/server.js)، لذلك لا يحتاج فحص السلامة إلى ترويسة مصادقة.

الاقتران: open-wa غير رسمي وغير تابع لـ WhatsApp — والرقم المتصل معرض لخطر الحظر بسبب آليات اكتشاف الأتمتة الخاصة بـ WhatsApp. عند التشغيل لأول مرة، تُطبع شفرة QR الخاصة بالاقتران إلى stdout وتُعرض عبر لوحة السجلات/تدفق SSE الموجود — ولا توجد بعد نقطة نهاية مخصصة لصورة QR في هذا التكامل.


4.7 الوكيل العكسي (تضمين لوحة معلومات 9Router)

تُضمّن لوحة المعلومات واجهة ويب 9Router داخل iframe عبر وكيل عكسي داخلي على:

GET|POST|... /dashboard/providers/services/9router/embed/[...path]

يقوم هذا الوكيل بما يلي:

  • يعيد توجيه الطلب إلى http://127.0.0.1:{port}/{path} (عبر الاسترجاع الحلقي فقط)
  • يزيل ترويستي cookie وauthorization الواردتين (لمنع تسرب جلسة OmniRoute)
  • يحقن Authorization: Bearer {apiKey} لمصادقة 9Router
  • يزيل set-cookie وcontent-security-policy وx-frame-options وcross-origin-* من الاستجابة
  • يعيد كتابة استجابات HTML لحقن <base href> وتوحيد المسارات المطلقة (/foo → /dashboard/.../embed/foo)

تتولى خادومة مصاحبة على منفذ مخصص معالجة ترقيات WebSocket الخاصة بلوحة المعلومات المضمّنة (راجع src/lib/services/embedWsProxy.ts).

الأمان: تُصنّف مسارات وكيل التضمين ضمن LOCAL_ONLY_API_PREFIXES ولا يمكن الوصول إليها إلا من خلال الاسترجاع الحلقي. ولا يمكن لمهاجم يحصل على JWT عبر نفق Cloudflare/Ngrok استخدام الوكيل للوصول إلى الخدمات المضمّنة.


5. الأمان

فرض LOCAL_ONLY (القاعدة الصارمة #17)

تُصنَّف جميع المسارات الواقعة ضمن /api/services/ و/dashboard/providers/services/*/embed/ على أنها LOCAL_ONLY في src/server/authz/routeGuard.ts. يُنفَّذ فحص عنوان الاسترجاع دون شروط قبل أي فرع للمصادقة:

يصل الطلب
  → isLocalOnlyPath(path)?
      → ليس عنوان استرجاع → 403 LOCAL_ONLY (دائمًا، قبل فحص المصادقة)
      → عنوان استرجاع     → المتابعة إلى المصادقة العادية

يمنع هذا استخدام JWT مُسرَّب (مثلًا عبر نفق) لتشغيل npm install أو إنشاء العمليات. راجع docs/security/ROUTE_GUARD_TIERS.md للاطلاع على مصفوفة المستويات الكاملة.

حقن مفتاح API

يتطلب 9Router وMux مفتاح API/رمز حامل لنقاط نهاية HTTP الخاصة بهما. يقوم OmniRoute بما يلي:

  1. ينشئ مفتاحًا عبر crypto.randomBytes(32).toString("base64url") مع بادئة خاصة بالخدمة (nr_ لـ9Router، وmx_ لـMux).
  2. يشفّره عند التخزين باستخدام AES-256-GCM (وهي خوارزمية التشفير نفسها المستخدمة لبيانات اعتماد المزوّد).
  3. يفك تشفيره ويحقنه بصفته متغير بيئة عند إنشاء العملية — NINEROUTER_API_KEY لـ9Router، وMUX_SERVER_AUTH_TOKEN لـMux (وليس أبدًا كخيار CLI، ولذلك لا يظهر الرمز مطلقًا في ps/قوائم العمليات).
  4. لا يعيد مفتاح النص الصريح مطلقًا في أي استجابة HTTP.

يتلقى CLIProxyAPI مفتاحًا مخصصًا لمستوى البيانات يُحقن عند إنشاء العملية (needsApiKey: true — يُستخدم لمزامنة النماذج مع المحوّل).

الحماية من SSRF

وُكِّل وكيل HTTP العكسي (/dashboard/.../embed/[...path]) برمجيًا لإعادة التوجيه فقط إلى http://127.0.0.1:{port}. ولا يتبع مطلقًا عمليات إعادة التوجيه إلى وجهات ليست ضمن نطاق الاسترجاع. تُستخدم مكتبة ssrf-req-filter لرفض أي عنوان URL علوي يُحل إلى عنوان خارج نطاق الاسترجاع.

سلامة الصدفة (القاعدة الصارمة #13)

يُستدعى npm install عبر execFile('npm', ['install', pkg, '--prefix', dir]) — من دون قوالب نصية، أو صدفة، أو استيفاء لمسارات خارجية في سلسلة الأمر. تُمرَّر قيم وقت التشغيل (المنافذ ومفاتيح API) عبر كائن env الخاص بالعملية الفرعية.

تنقية الأخطاء (القاعدة الصارمة #12)

تمر جميع استجابات الأخطاء من /api/services/* عبر buildErrorBody() أو sanitizeErrorMessage(). ولا تُعاد قيمتا err.stack وerr.message الخام حرفيًا إلى المستدعي مطلقًا.


6. إضافة خدمة مضمنة جديدة

اتبع هذه الخطوات الثماني. اقرأ عمليات التنفيذ الحالية في src/lib/services/installers/ وsrc/app/api/services/ بوصفها المرجع المعتمد.

الخطوة 1 — إنشاء المثبّت

أنشئ src/lib/services/installers/{name}.ts على غرار ninerouter.ts:

export const NAME_PACKAGE = "your-npm-package";
export const NAME_DEFAULT_PORT = 20132; // اختر منفذًا متاحًا

export async function install(version = "latest"): Promise<InstallResult> { ... }
export async function update(version = "latest"): Promise<InstallResult> { ... }
export async function uninstall(): Promise<void> { ... }
export function resolveSpawnArgs(apiKey: string, port: number): SpawnArgs { ... }
export async function getInstalledVersion(): Promise<string | null> { ... }
export async function getLatestVersion(): Promise<string | null> { ... }

استخدم runNpm(['install', NAME_PACKAGE, '--prefix', dir]) من installers/utils.ts — ولا تستخدم أبدًا execSync أو الاستيفاء عبر الصدفة.

الخطوة 2 — التسجيل في التمهيد

أضف ServiceEntry إلى مصفوفة SERVICES في src/lib/services/bootstrap.ts:

{
  tool: "myservice",
  port: NAME_DEFAULT_PORT,
  healthPath: "/health",
  healthIntervalMs: 5_000,
  stopTimeoutMs: 15_000,
  logsBufferBytes: 5_242_880,
  needsApiKey: true, // false إذا لم تكن هناك حاجة إلى مفتاح API
}

وسّع buildSpawnArgsFactory() لمعالجة cfg.tool === "myservice".

عقد إضافة المزوّد القابل للتركيب (المرحلة 1، #7333)

يقدّم src/lib/services/providerPlugins/ عقد ServiceProviderPlugin الذي يجمع حقول ServiceEntry الخاصة بـbootstrap.ts للواجهة الخلفية وحقول قالب البيان في serviceBackends.ts ضمن كائن واحد، بدلًا من التعبير عن بنية الواجهة الخلفية نفسها بشكل منفصل في ملفين غير مرتبطين. حتى وقت كتابة هذا النص، لم يُرحَّل سوى 9router — إذ يستمد bootstrap.ts إدخال SERVICES[] الخاص به من getServiceProviderPlugin("9router") (src/lib/services/providerPlugins/registry.ts)، مع طرح خطأ عند بدء التشغيل إذا أصبحت الإضافة مفقودة في أي وقت. وتظل cliproxy وmux وbifrost على القيم الحرفية المضمنة الموجودة مسبقًا في SERVICES[] دون تغيير.

حصل open-sse/config/providerPluginManifest.ts أيضًا على دالة مساعدة إضافية createServiceBackendManifestEntry(pluginId, template) تنشئ ProviderPluginManifestEntry صحيح البنية من إدخال SERVICE_BACKEND_MANIFEST_TEMPLATE — وهي غير موصولة حتى الآن بأي مسار طلب فعلي (لا generateProviderPluginManifestFromRegistry() ولا /v1/providers/[provider]/models)؛ ويظل ذلك عملًا لاحقًا بعد إثبات صلاحية العقد لواجهة خلفية ثانية.

أُجِّلت إلى طلبات سحب لاحقة، ويجري تتبعها ضمن المشكلة #7333: ترحيل cliproxyapi عبر السجل نفسه، وتعميم mux/bifrost ضمن اتحاد ServiceBackendPluginId، ودمج المعالجة الخاصة بتوجيه المنفّذ (open-sse/executors/index.ts، open-sse/handlers/chatCore/executorProxy.ts) في عقد الإضافة، وربط createServiceBackendManifestEntry() بمسار فعلي للبيان/النماذج.

الخطوة 3 — إضافة الترحيل وبيانات DB الأولية

تأكد من وجود صف للخدمة في version_manager عبر ترحيل في src/lib/db/migrations/. يجب أن يحتوي الصف على:

INSERT OR IGNORE INTO version_manager (tool, status, auto_start, provider_expose)
VALUES ('myservice', 'not_installed', 0, 0);

الخطوة 4 — إنشاء نقاط نهاية API السبع

ضمن src/app/api/services/{name}/:

_lib.ts            دالة getOrInitSupervisor() المساعدة
install/route.ts   POST — يستدعي installer.install()
start/route.ts     POST — يستدعي supervisor.start()
stop/route.ts      POST — يستدعي supervisor.stop()
restart/route.ts   POST — يستدعي supervisor.restart()
update/route.ts    POST — يستدعي installer.update()
status/route.ts    GET  — يدمج الحالة المباشرة مع حالة DB
auto-start/route.ts POST — يبدّل علامة auto_start

مسار GET /api/services/[name]/logs المشترك موصول بالفعل — لا حاجة إلى أي تغييرات هناك.

فوّض جميع استجابات الأخطاء عبر createErrorResponse() / buildErrorBody().

الخطوة 5 — الإضافة إلى LOCAL_ONLY_API_PREFIXES

في src/server/authz/routeGuard.ts، تحقّق من أن /api/services/ مُدرج بالفعل. إذا أضفت بادئة جديدة (مثل /api/tools/)، فأضفها إلى كلٍ من LOCAL_ONLY_API_PREFIXES وSPAWN_CAPABLE_PREFIXES إذا كانت تُنشئ عمليات. أضف اختبارًا في tests/unit/authz/routeGuard.test.ts.

الخطوة 6 — إضافة علامة تبويب واجهة المستخدم

أنشئ src/app/(dashboard)/dashboard/providers/services/tabs/{Name}ServiceTab.tsx. أعِد استخدام المكوّنات المشتركة:

  • ServiceStatusCard — الحالة المباشرة + شارة السلامة
  • ServiceLifecycleButtons — بدء / إيقاف / إعادة تشغيل / تحديث
  • ServiceLogsPanel — متابعة السجل عبر SSE (يتصل بـ /api/services/{name}/logs)
  • ApiKeyCard — إظهار المفتاح + تدويره (إذا كانت needsApiKey: true)

سجّل علامة التبويب في ServicesPageShell.tsx.

الخطوة 7 — إضافة مُدخل المزوّد (إذا كانت الخدمة هدفًا للتوجيه)

إذا كانت الخدمة المضمّنة توفّر نقطة نهاية /v1/chat/completions متوافقة مع OpenAI:

  1. أضف مُدخل مزوّد في src/shared/constants/providers.ts مع isEmbeddedService: true.
  2. أنشئ open-sse/executors/{name}.ts يمتد من BaseExecutor. أعِد البحث عن المنفذ ومفتاح API لكل طلب (ولا تخزّنهما مؤقتًا في المُنشئ مطلقًا). أعِد استجابة 503 service_not_running عندما لا تكون حالة المشرف "running".
  3. سجّل النماذج في open-sse/config/providerRegistry.ts باستخدام بادئة الخدمة (مثل myservice/sub/model). سيحافظ modelSync.ts على تحديثها.

الخطوة 8 — التوثيق والاختبار

  1. حدّث docs/frameworks/EMBEDDED-SERVICES.md (هذا الملف) — أضف الخدمة إلى الجدول في §1 وأي نقاط نهاية جديدة إلى §4.
  2. أضف اختبارات وحدة في tests/unit/services/ (دورة الحياة، أداة التثبيت، بنية API).
  3. أضف اختبار تكامل في tests/integration/services/ (خلف RUN_SERVICES_INT=1).
  4. حدّث docs/openapi.yaml بنقاط النهاية الجديدة.

7. استكشاف الأخطاء وإصلاحها

الخدمة لا تبدأ

الأعراض: يُرجع زر البدء الحالة 503، وتظل الحالة "error" أو "starting".

قائمة التحقق:

  1. تحقّق من GET /api/services/{name}/logs (أو لوحة السجلات في لوحة المعلومات). ابحث عن أسطر مثل Error: ENOENT أو address already in use أو Cannot find module.
  2. تحقّق من وجود npm في PATH: نفّذ which npm من حساب المستخدم نفسه الذي يشغّل OmniRoute.
  3. تحقّق من تثبيت الخدمة: افحص GET /api/services/{name}/status بحثًا عن installedVersion. إذا كانت null، فنفّذ التثبيت أولًا.
  4. تحقّق من أن DATA_DIR/services/{name}/node_modules/ موجود وليس فارغًا.
  5. تحقّق من الحقل lastError في استجابة الحالة لمعرفة سبب الخروج المنقّح.

بدء التشغيل البارد بطيء (> 10 ثوانٍ للوصول إلى running)

الأعراض: تظل الحالة "starting" مدة طويلة قبل أن تنتقل إلى "running" أو "error".

التفسير: يتضمن بدء التشغيل البارد لـ 9Router استيراد أشجار اعتمادات كبيرة (وحدات DNS والنفق وMITM). الفاصل الزمني الافتراضي لفحص الصحة هو ثانيتان، مع 3 محاولات قبل أن تعلن عملية الإشراف انتهاء المهلة (لكنها تواصل الاستقصاء).

الإصلاح: يمكن ضبط healthIntervalMs ومهلة waitForHealthy (healthIntervalMs * 3) في bootstrap.ts. بالنسبة إلى الخدمات التي تستغرق وقتًا أطول لبدء التشغيل، زِد healthIntervalMs إلى 5000 وstopTimeoutMs إلى 30 000.


تعارض المنفذ (EADDRINUSE)

الأعراض: تعرض السجلات address already in use :::20130.

الأسباب:

  • هناك عملية أخرى تستخدم المنفذ 20130 بالفعل.
  • لم تتوقف عملية 9Router سابقة بالكامل (PID متروك).

الإصلاح:

  1. غيّر المنفذ الافتراضي عبر متغير البيئة NINEROUTER_PORT في .env.
  2. ابحث عن العملية المتعارضة وأنهِها: lsof -ti :20130 | xargs kill -9.
  3. يمكن ضبط المنفذ لكل خدمة في bootstrap.ts عبر الحقل port.

ملاحظة: يستخدم 9Router المنفذ 20130 افتراضيًا، تحديدًا لتجنب التعارض مع المنفذ الافتراضي 20128 لـ OmniRoute.


رفض الإذن (EACCES) عند التثبيت

الأعراض: يُرجع التثبيت الحالة 500، وتعرض السجلات EACCES أو permission denied.

الأسباب:

  • لا يملك مسار DATA_DIR أو المجلد الأب له صلاحية الكتابة بواسطة عملية OmniRoute.
  • التشغيل داخل Docker دون صلاحيات الجذر ومن دون إمكانية الكتابة إلى وحدة التخزين المعيّنة.

الإصلاح:

  1. تحقّق من DATA_DIR (الافتراضي: ~/.omniroute/): ls -la ~/.omniroute/
  2. تأكّد من أن مستخدم عملية OmniRoute يملك المجلد: chown -R $USER ~/.omniroute/
  3. في Docker، تأكّد من أن وحدة التخزين المركّبة تملك الأذونات الصحيحة لمستخدم الحاوية.

فشل التحديث (انتهاء مهلة npm install أو خطأ في الشبكة)

الأعراض: يُرجع التحديث الحالة 500 مع InstallError، وتعرض السجلات انتهاء مهلة الشبكة.

قائمة التحقق:

  1. تأكّد من إمكانية الوصول إلى سجل npm: npm ping.
  2. تحقّق من وجود وكيل مؤسسي: npm config get proxy وnpm config get https-proxy.
  3. جرّب التثبيت يدويًا: npm install {package}@latest --prefix ~/.omniroute/services/{name}/.
  4. إذا كنت خلف شبكة معزولة، فنزّل حزمة tarball مسبقًا واستخدم npm install /path/to/tarball.tgz.

تعرض الخدمة الحالة "error" فورًا بعد البدء (تعطل سريع)

الأعراض: تنتقل الحالة من "starting" إلى "error" في أقل من 5 ثوانٍ. يعرض lastError القيمة "Fast crash (exited with code 1)".

قائمة التحقق:

  1. اقرأ ذيل السجل الكامل: GET /api/services/{name}/logs?tail=500.
  2. سبب شائع: متغيرات البيئة التي تتوقعها الخدمة مفقودة.
  3. بالنسبة إلى 9Router: تحقّق من تمرير NINEROUTER_DISABLE_MITM=true و NINEROUTER_DISABLE_TUNNEL=true في البيئة عند إنشاء العملية (راجع resolveSpawnArgs في installers/ninerouter.ts).

8. الأسئلة الشائعة

س: هل يمكنني إتاحة نقاط نهاية الخدمات المضمّنة للعملاء من خارج واجهة الاسترجاع؟

لا. فئة LOCAL_ONLY مقصودة (القاعدة الصارمة رقم 17). يجب ألا تكون المسارات التي يمكنها استدعاء npm install أو إنشاء عمليات node قابلة للوصول من حركة بيانات آتية من خارج واجهة الاسترجاع، لأن تسرّب JWT عبر نفق (Cloudflare أو Ngrok أو Tailscale) سيتيح بخلاف ذلك إنشاء عمليات عشوائية. لا يوجد استثناء اختياري للمسار /api/services/ — فعلى خلاف /api/mcp/، هو مستبعد من قائمة تجاوز نطاق الإدارة. راجع docs/security/ROUTE_GUARD_TIERS.md.


س: هل ستتوفر 9Router وCLIProxyAPI في عمليات النشر الإنتاجية/السحابية؟

نعم. تتبع كلتا الخدمتين النموذج المحلي أولًا نفسه الذي يتبعه OmniRoute. وتعملان على الجهاز نفسه وتتواصلان عبر واجهة الاسترجاع. ويُقصد بمصطلح «الإنتاج» هنا خادم VPS أو الخادم المحلي الذي نُشر عليه OmniRoute، وليس مزوّد خدمات سحابية بعيدًا.


س: كيف يمكنني تصحيح أخطاء المشرف؟

  1. تابع تدفق سجل SSE: curl -N http://localhost:20128/api/services/9router/logs.
  2. افحص السجلات المهيكلة في مخرجات pino الخاصة بـ OmniRoute بعد تصفيتها حسب مساحة الاسم service:supervisor.
  3. افحص صف قاعدة البيانات: sqlite3 ~/.omniroute/omniroute.db "SELECT * FROM version_manager WHERE tool='9router'".
  4. استخدم GET /api/services/9router/status للاطلاع على الحالة المباشرة الحالية ومعرّف العملية PID والصحة وlastError في استدعاء واحد.

س: يعرض المشرف health: "degraded" أو health: "unknown"، لكن الحالة هي "running". هل هذه مشكلة؟

تعني "degraded" أن فحص الصحة أعاد استجابة لا تحمل الرمز 200. وتعني "unknown" أنه لم يكتمل أي فحص بعد (حالة تسابق مع أول استقصاء). وكلتا الحالتين مؤقتتان أثناء بدء التشغيل. إذا ظلت الصحة في الحالة "degraded" لأكثر من healthIntervalMs * 3 مللي ثانية بعد "running"، فهذا يعني أن الخدمة المضمّنة تعمل، لكن واجهة HTTP API الخاصة بها لا تستجيب. تحقّق مما إذا كان المنفذ صحيحًا في استجابة الحالة، وما إذا كانت الخدمة تستمع بالفعل على ذلك المنفذ.


س: هل يمكنني تغيير مفتاح API الخاص بـ 9Router دون إعادة تشغيل كاملة؟

لا. يُمرَّر مفتاح API إلى 9Router عبر متغير بيئة عند إنشاء العملية. ولا يمكن تغيير متغيرات البيئة في عملية قيد التشغيل. يوقف POST .../rotate-key الخدمة ويعيد تشغيلها تلقائيًا لتطبيق المفتاح الجديد. ويصبح تدوير المفتاح ساريًا خلال مدة stopTimeoutMs الخاصة بالخدمة (15 ثانية افتراضيًا)، بالإضافة إلى وقت بدء تشغيلها.


س: ما حد المخزن المؤقت الحلقي، وماذا يحدث عند امتلائه؟

لكل خدمة مخزن مؤقت حلقي مخصص بسعة 5 MB. وعندما يمتلئ المخزن المؤقت، تُزال أقدم أسطر السجل لإفساح المجال للأسطر الجديدة. يعيد حدث SSE المسمى snapshot أحدث الأسطر ضمن حد tail. ولا تُحفظ السجلات على القرص إلا إذا ضُبط logsBufferPath في صف قاعدة البيانات.


انظر أيضًا

  • docs/security/ROUTE_GUARD_TIERS.md — تفاصيل فئة LOCAL_ONLY
  • docs/architecture/CODEBASE_DOCUMENTATION.md — §3.2 تخطيط وحدة الخدمات المضمّنة
  • docs/architecture/ARCHITECTURE.md — السياق على مستوى النظام
  • docs/openapi.yaml — تعريفات نقاط النهاية القابلة للقراءة آليًا
  • CLAUDE.md §«إضافة خدمة مضمّنة جديدة» — قائمة تحقق مرجعية سريعة