🌐 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 وتتواصل عبر واجهة الاسترجاع الحلقي.
- نظرة عامة
- البنية — 4 طبقات
- آلة حالات دورة الحياة
- مرجع API
- الأمان
- إضافة خدمة مضمّنة جديدة
- استكشاف الأخطاء وإصلاحها
- الأسئلة الشائعة
تم تضمين ست خدمات:
| الخدمة | حزمة 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 (ثلاث علامات تبويب) |
| التشغيل التلقائي | مفتاح تبديل لكل خدمة، ويكون معطّلًا افتراضيًا |
┌────────────────────────────────────────────────────────────────────┐
│ الطبقة 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) |
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() المتزامنة على المشرف نفسه إلى إنشاء
عملية واحدة فقط؛ إذ ينتظر المستدعي الثاني ويُرجع الحالة الحالية. يمنع ذلك
حالات التسابق عندما يحدث، على سبيل المثال، التشغيل التلقائي والضغط على زر في واجهة المستخدم في الوقت نفسه.
جميع المسارات ضمن /api/services/ هي LOCAL_ONLY (واجهة الاسترجاع فقط، القاعدة الصارمة رقم 17).
تتلقى الطلبات الواردة من خارج واجهة الاسترجاع 403 LOCAL_ONLY بغض النظر عن رمز المصادقة.
تثبيت 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 في صورة رسائل واضحة.
بدء تشغيل 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
}إيقاف 9Router بأمان. يرسل SIGTERM، وينتظر 15 ثانية، ثم يرسل SIGKILL إذا ظلت العملية قيد التشغيل. العملية متكررة آمنة إذا كانت الخدمة متوقفة بالفعل.
نص الطلب: لا يوجد
الاستجابات:
| الحالة | الوصف |
|---|---|
200 |
ServiceStatus (الحالة: "stopped") |
503 |
فشل الإيقاف على نحو غير متوقع |
يكافئ تنفيذ stop() ثم start() ضمن قفل العملية.
نص الطلب: لا يوجد
الاستجابات: مماثلة لـ start (تعيد كائن ServiceStatus النهائي).
يحدّث 9Router إلى إصدار npm أحدث. إذا كانت الخدمة قيد التشغيل، فتُوقَف أولًا، ثم يُشغَّل تثبيت npm (لتثبيت الإصدار الأحدث في الموضع نفسه)، وبعد ذلك تُعاد عملية تشغيل الخدمة.
نص الطلب (جميع الحقول اختيارية):
{ "version": "latest" }الاستجابات:
| الحالة | الوصف |
|---|---|
200 |
{ ok: true, previousVersion: "...", installedVersion: "..." } |
400 |
نص غير صالح |
500 |
فشل تحديث npm |
ينشئ مفتاح API جديدًا لـ 9Router، ويشفّره عند التخزين، ويعيد تشغيل الخدمة (إذا كانت قيد التشغيل) لكي تستخدم المفتاح الجديد من بيئتها. يُلغى المفتاح القديم فورًا.
نص الطلب: لا يوجد
الاستجابات:
| الحالة | الوصف |
|---|---|
200 |
{ keyRotated: true, restarted: boolean } |
500 |
فشل تدوير المفتاح |
الأمان: لا يُعاد المفتاح الجديد مطلقًا في الاستجابة (لمنع تسريب بيانات الاعتماد).
ويُخزَّن مشفرًا (AES-256-GCM) في جدول version_manager.
يعيد الحالة المجمعة المباشرة ومن قاعدة البيانات، بما يشمل بيانات تعريف الإصدار ومعاينة مفتاح 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
}تبديل علامة البدء التلقائي. عندما تكون enabled: true، تبدأ الخدمة تلقائيًا
في المرة التالية التي يبدأ فيها OmniRoute (إذا كانت الخدمة مثبتة).
نص الطلب:
{ "enabled": true }الاستجابات:
| الحالة | الوصف |
|---|---|
200 |
{ autoStart: true } |
400 |
نص غير صالح |
تدفق 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 |
لم يتم العثور على الخدمة (المشرف غير مسجّل) |
يمتلك 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].
يمتلك 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 |
تبديل التشغيل التلقائي |
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 الصريح.
بنية دورة الحياة نفسها الخاصة بالخدمات الأخرى (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).
يشغّل 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 في هذا التكامل.
تُضمّن لوحة المعلومات واجهة ويب 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 استخدام الوكيل للوصول إلى الخدمات المضمّنة.
تُصنَّف جميع المسارات الواقعة ضمن /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 للاطلاع على مصفوفة
المستويات الكاملة.
يتطلب 9Router وMux مفتاح API/رمز حامل لنقاط نهاية HTTP الخاصة بهما. يقوم OmniRoute بما يلي:
- ينشئ مفتاحًا عبر
crypto.randomBytes(32).toString("base64url")مع بادئة خاصة بالخدمة (nr_لـ9Router، وmx_لـMux). - يشفّره عند التخزين باستخدام AES-256-GCM (وهي خوارزمية التشفير نفسها المستخدمة لبيانات اعتماد المزوّد).
- يفك تشفيره ويحقنه بصفته متغير بيئة عند إنشاء العملية —
NINEROUTER_API_KEYلـ9Router، وMUX_SERVER_AUTH_TOKENلـMux (وليس أبدًا كخيار CLI، ولذلك لا يظهر الرمز مطلقًا فيps/قوائم العمليات). - لا يعيد مفتاح النص الصريح مطلقًا في أي استجابة HTTP.
يتلقى CLIProxyAPI مفتاحًا مخصصًا لمستوى البيانات يُحقن عند إنشاء العملية
(needsApiKey: true — يُستخدم لمزامنة النماذج مع المحوّل).
وُكِّل وكيل HTTP العكسي (/dashboard/.../embed/[...path]) برمجيًا لإعادة التوجيه
فقط إلى http://127.0.0.1:{port}. ولا يتبع مطلقًا عمليات إعادة التوجيه إلى وجهات
ليست ضمن نطاق الاسترجاع. تُستخدم مكتبة ssrf-req-filter لرفض أي عنوان URL علوي
يُحل إلى عنوان خارج نطاق الاسترجاع.
يُستدعى npm install عبر execFile('npm', ['install', pkg, '--prefix', dir]) —
من دون قوالب نصية، أو صدفة، أو استيفاء لمسارات خارجية في سلسلة الأمر.
تُمرَّر قيم وقت التشغيل (المنافذ ومفاتيح API) عبر كائن env الخاص بالعملية الفرعية.
تمر جميع استجابات الأخطاء من /api/services/* عبر buildErrorBody() أو
sanitizeErrorMessage(). ولا تُعاد قيمتا err.stack وerr.message الخام
حرفيًا إلى المستدعي مطلقًا.
اتبع هذه الخطوات الثماني. اقرأ عمليات التنفيذ الحالية في src/lib/services/installers/
وsrc/app/api/services/ بوصفها المرجع المعتمد.
أنشئ 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 أو الاستيفاء عبر الصدفة.
أضف 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".
يقدّم 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() بمسار فعلي للبيان/النماذج.
تأكد من وجود صف للخدمة في version_manager عبر ترحيل في
src/lib/db/migrations/. يجب أن يحتوي الصف على:
INSERT OR IGNORE INTO version_manager (tool, status, auto_start, provider_expose)
VALUES ('myservice', 'not_installed', 0, 0);ضمن 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().
في src/server/authz/routeGuard.ts، تحقّق من أن /api/services/ مُدرج بالفعل.
إذا أضفت بادئة جديدة (مثل /api/tools/)، فأضفها إلى كلٍ من
LOCAL_ONLY_API_PREFIXES وSPAWN_CAPABLE_PREFIXES إذا كانت تُنشئ عمليات.
أضف اختبارًا في tests/unit/authz/routeGuard.test.ts.
أنشئ src/app/(dashboard)/dashboard/providers/services/tabs/{Name}ServiceTab.tsx.
أعِد استخدام المكوّنات المشتركة:
ServiceStatusCard— الحالة المباشرة + شارة السلامةServiceLifecycleButtons— بدء / إيقاف / إعادة تشغيل / تحديثServiceLogsPanel— متابعة السجل عبر SSE (يتصل بـ/api/services/{name}/logs)ApiKeyCard— إظهار المفتاح + تدويره (إذا كانتneedsApiKey: true)
سجّل علامة التبويب في ServicesPageShell.tsx.
إذا كانت الخدمة المضمّنة توفّر نقطة نهاية /v1/chat/completions متوافقة مع OpenAI:
- أضف مُدخل مزوّد في
src/shared/constants/providers.tsمعisEmbeddedService: true. - أنشئ
open-sse/executors/{name}.tsيمتد منBaseExecutor. أعِد البحث عن المنفذ ومفتاح API لكل طلب (ولا تخزّنهما مؤقتًا في المُنشئ مطلقًا). أعِد استجابة503 service_not_runningعندما لا تكون حالة المشرف"running". - سجّل النماذج في
open-sse/config/providerRegistry.tsباستخدام بادئة الخدمة (مثلmyservice/sub/model). سيحافظmodelSync.tsعلى تحديثها.
- حدّث
docs/frameworks/EMBEDDED-SERVICES.md(هذا الملف) — أضف الخدمة إلى الجدول في §1 وأي نقاط نهاية جديدة إلى §4. - أضف اختبارات وحدة في
tests/unit/services/(دورة الحياة، أداة التثبيت، بنية API). - أضف اختبار تكامل في
tests/integration/services/(خلفRUN_SERVICES_INT=1). - حدّث
docs/openapi.yamlبنقاط النهاية الجديدة.
الأعراض: يُرجع زر البدء الحالة 503، وتظل الحالة "error" أو "starting".
قائمة التحقق:
- تحقّق من
GET /api/services/{name}/logs(أو لوحة السجلات في لوحة المعلومات). ابحث عن أسطر مثلError: ENOENTأوaddress already in useأوCannot find module. - تحقّق من وجود
npmفي PATH: نفّذwhich npmمن حساب المستخدم نفسه الذي يشغّل OmniRoute. - تحقّق من تثبيت الخدمة: افحص
GET /api/services/{name}/statusبحثًا عنinstalledVersion. إذا كانتnull، فنفّذ التثبيت أولًا. - تحقّق من أن
DATA_DIR/services/{name}/node_modules/موجود وليس فارغًا. - تحقّق من الحقل
lastErrorفي استجابة الحالة لمعرفة سبب الخروج المنقّح.
الأعراض: تظل الحالة "starting" مدة طويلة قبل أن تنتقل إلى "running" أو "error".
التفسير: يتضمن بدء التشغيل البارد لـ 9Router استيراد أشجار اعتمادات كبيرة (وحدات DNS والنفق وMITM). الفاصل الزمني الافتراضي لفحص الصحة هو ثانيتان، مع 3 محاولات قبل أن تعلن عملية الإشراف انتهاء المهلة (لكنها تواصل الاستقصاء).
الإصلاح: يمكن ضبط healthIntervalMs ومهلة waitForHealthy
(healthIntervalMs * 3) في bootstrap.ts. بالنسبة إلى الخدمات التي تستغرق وقتًا أطول
لبدء التشغيل، زِد healthIntervalMs إلى 5000 وstopTimeoutMs إلى 30 000.
الأعراض: تعرض السجلات address already in use :::20130.
الأسباب:
- هناك عملية أخرى تستخدم المنفذ 20130 بالفعل.
- لم تتوقف عملية 9Router سابقة بالكامل (PID متروك).
الإصلاح:
- غيّر المنفذ الافتراضي عبر متغير البيئة
NINEROUTER_PORTفي.env. - ابحث عن العملية المتعارضة وأنهِها:
lsof -ti :20130 | xargs kill -9. - يمكن ضبط المنفذ لكل خدمة في
bootstrap.tsعبر الحقلport.
ملاحظة: يستخدم 9Router المنفذ 20130 افتراضيًا، تحديدًا لتجنب التعارض مع المنفذ الافتراضي 20128 لـ OmniRoute.
الأعراض: يُرجع التثبيت الحالة 500، وتعرض السجلات EACCES أو permission denied.
الأسباب:
- لا يملك مسار
DATA_DIRأو المجلد الأب له صلاحية الكتابة بواسطة عملية OmniRoute. - التشغيل داخل Docker دون صلاحيات الجذر ومن دون إمكانية الكتابة إلى وحدة التخزين المعيّنة.
الإصلاح:
- تحقّق من
DATA_DIR(الافتراضي:~/.omniroute/):ls -la ~/.omniroute/ - تأكّد من أن مستخدم عملية OmniRoute يملك المجلد:
chown -R $USER ~/.omniroute/ - في Docker، تأكّد من أن وحدة التخزين المركّبة تملك الأذونات الصحيحة لمستخدم الحاوية.
الأعراض: يُرجع التحديث الحالة 500 مع InstallError، وتعرض السجلات انتهاء مهلة الشبكة.
قائمة التحقق:
- تأكّد من إمكانية الوصول إلى سجل npm:
npm ping. - تحقّق من وجود وكيل مؤسسي:
npm config get proxyوnpm config get https-proxy. - جرّب التثبيت يدويًا:
npm install {package}@latest --prefix ~/.omniroute/services/{name}/. - إذا كنت خلف شبكة معزولة، فنزّل حزمة tarball مسبقًا واستخدم
npm install /path/to/tarball.tgz.
الأعراض: تنتقل الحالة من "starting" إلى "error" في أقل من 5 ثوانٍ.
يعرض lastError القيمة "Fast crash (exited with code 1)".
قائمة التحقق:
- اقرأ ذيل السجل الكامل:
GET /api/services/{name}/logs?tail=500. - سبب شائع: متغيرات البيئة التي تتوقعها الخدمة مفقودة.
- بالنسبة إلى 9Router: تحقّق من تمرير
NINEROUTER_DISABLE_MITM=trueوNINEROUTER_DISABLE_TUNNEL=trueفي البيئة عند إنشاء العملية (راجعresolveSpawnArgsفيinstallers/ninerouter.ts).
س: هل يمكنني إتاحة نقاط نهاية الخدمات المضمّنة للعملاء من خارج واجهة الاسترجاع؟
لا. فئة 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، وليس مزوّد خدمات سحابية بعيدًا.
س: كيف يمكنني تصحيح أخطاء المشرف؟
- تابع تدفق سجل SSE:
curl -N http://localhost:20128/api/services/9router/logs. - افحص السجلات المهيكلة في مخرجات pino الخاصة بـ OmniRoute بعد تصفيتها حسب
مساحة الاسم
service:supervisor. - افحص صف قاعدة البيانات:
sqlite3 ~/.omniroute/omniroute.db "SELECT * FROM version_manager WHERE tool='9router'". - استخدم
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_ONLYdocs/architecture/CODEBASE_DOCUMENTATION.md— §3.2 تخطيط وحدة الخدمات المضمّنةdocs/architecture/ARCHITECTURE.md— السياق على مستوى النظامdocs/openapi.yaml— تعريفات نقاط النهاية القابلة للقراءة آليًاCLAUDE.md§«إضافة خدمة مضمّنة جديدة» — قائمة تحقق مرجعية سريعة