تثبيت Nessie
شغّل Nessie على بنية تحتية تختارها، من جهاز واحد إلى سحابة مُدارة.
ما تحتاجه قبل أن تبدأ
Nessie مستودع pnpm أحادي يضم خدمات Node وحزمتين ثابتتين. أيًّا كانت طريقة التشغيل، يجب أن تتوفر أولًا الأمور الأربعة نفسها.
- PostgreSQL مع
pgvector. ليس اختياريًا ولا يمكن استبداله، فعمليات الترحيل تنفّذCREATE EXTENSION vectorوتنشئ أعمدةvector(1024). وتعمل بيئة الإنتاج بالصورةpgvector/pgvector:pg17. - Node 22 وpnpm. كل Dockerfile في
infrastructure/docker/يُبنى علىnode:22-slim، ويحدد المستودع مدير الحزم في ملفpackage.jsonالجذري (pnpm@10.22.0). - نقطة نهاية نموذج متوافقة مع OpenAI ومفتاح للوصول إليها. تمر عبرها الوكلاء والتضمينات معًا. راجع «ضبط الاستدلال» أدناه.
- تخزين كائنات متوافق مع S3، لأي نشر لا يكون بيئة المطور المحلية. ترفض الرفعات على القرص المحلي خارج وضع
local، ويكون هذا الرفض فشلًا عند بدء التشغيل وليس تشغيلًا متدهورًا. راجع «على جهاز واحد، أو في سحابة مُدارة».
تحتاج Docker وبوابة عكسية إلى مسار الحاويات الموصوف لاحقًا، كما تحتاج نطاقًا وشهادة TLS لأي خدمة متاحة من الإنترنت.
لا يذكر المستودع مواصفات العتاد، أي الحد الأدنى من المعالج والذاكرة والقرص لعدد معيّن من الأشخاص، لذلك لا تذكرها هذه الصفحة أيضًا.
المكونات، وكيف تترابط
| المكوّن | ماهيّته | الوصول الوارد |
|---|---|---|
api/ | واجهة HTTP وWebSocket المبنية على Fastify. يتصل بها كل عميل ولا يتصل بغيرها. | نعم |
worker/ | ينسّق الوكلاء ويشغّل المهام الخلفية. يستعلم طابور المهام، ولا يفتح أي منفذ. | لا |
admin/ | واجهة المنتج، وهي حزمة ثابتة يبنيها Vite ويقدّمها nginx. | نعم |
web/ | الموقع العام. ثابت، واختياري، ولا علاقة له بتشغيل المنتج. | نعم |
| PostgreSQL | قاعدة البيانات، وطابور المهام، وناقل الاتصال الفوري، ثلاثتها معًا. | لا |
| تخزين الكائنات | المرفقات والصور الشخصية وملفات قاعدة المعرفة. متوافق مع S3، وتستخدم بيئة الإنتاج MinIO. | لا |
gateway/ | وسيط اختياري لإرسال الإشعارات عبر APNs وFCM. يبقى متوقفًا ما لم تفعّله عن قصد. | نعم |
الـAPI والعامل صورة واحدة. الحزمتان مترابطتان بإحكام، لذلك يبني Dockerfile.app الفريق كله مرة واحدة، وتتجاوز حاوية العامل الأمر إلى node worker/dist/index.js. وفي وضع local لا توجد حاوية عامل أصلًا، إذ تشغّل الـAPI العامل ضمن عمليتها نفسها.
لا يوجد Redis. طابور المهام وناقل الاتصال الفوري يعتمدان كلاهما على Postgres، وهذا قرار مقصود لا إغفال؛ فشجرة Google Cloud تحذف وحدات Pub/Sub وRedis للسبب نفسه.
المنافذ
في التطوير المحلي تعمل الـAPI افتراضيًا على المنفذ 5454، وتعمل لوحة الإدارة على 5455. ويمكن لنسخة العمل أن تأخذ زوجها الخاص بتعيين NESSIE_API_PORT وNESSIE_ADMIN_PORT، في البيئة أو في ملف .env بالمستودع؛ وكل أداة في المستودع، ومنها مجموعات اختبار المتصفح، تقرأ القيمتين نفسيهما. اختر الزوج قبل البدء والزمه: فنقل خدمة قيد التشغيل إلى منفذ آخر يجعل لوحة الإدارة تتحدث إلى API الخطأ.
الإنتاج أمر مختلف، ولا يتأثر بهذا الشرط. تستمع حاوية الـAPI داخليًا على المنفذ 5554 (مثبَّت بالمتغير NESSIE_API_PORT)، ويستمع وسيط الإشعارات على 5556، وكلاهما خلف الوكيل العكسي.
التشغيل على جهاز واحد للتطوير
يتضمن المستودع ملف Compose يحتوي على قاعدة بيانات فقط، وهذا كل ما تحتاجه حلقة التطوير المحلية.
pnpm install
docker compose -f infrastructure/compose/docker-compose.yml up -d postgresتنشر هذه الحاوية Postgres على المنفذ 55432، واسم المستخدم وكلمة المرور واسم قاعدة البيانات كلها nessie. ضع سلسلة الاتصال في ملف .env في جذر المستودع، فسكربت التطوير في الـAPI يقرؤه، ثم طبّق الترحيلات وابنِ العامل مرة واحدة، لأن الـAPI في الوضع المحلي تحمّل العامل من مجلد dist المبني:
DATABASE_URL=postgresql://nessie:nessie@localhost:55432/nessie
pnpm --filter @nessie/api prisma:migrate:deploy
pnpm --filter @nessie/worker build
pnpm devيشغّل pnpm dev الـAPI ولوحة الإدارة بالتوازي مع إعادة التحميل الفوري، على المنفذين 5454 و5455. تأكد من أن الـAPI استجابت للطلب GET /api/health.
Postgres ومتطلب pgvector
تنشئ الترحيلات نفسها ثلاث إضافات هي vector وpg_trgm وpgcrypto، لذلك يجب السماح للدور الذي ينفّذها بإنشاء الإضافات. وفي Postgres المُدار، هذا أول ما يجب التحقق منه قبل أي شيء آخر.
عرض المتجهات جزء من المخطط، وليس إعدادًا قابلًا للتغيير. الأعمدة thoughts.embedding وthought_recalls.query_embedding وknowledge_page_chunks.embedding من النوع vector(N)، ويُحدَّد N مرة واحدة باسم EMBEDDING_DIMENSIONS في الملف packages/schemas/src/embedding.ts، وقيمته الحالية 1024، وهو العرض الأصلي لنموذج التضمين الذي تستخدمه بيئة الإنتاج. ترسل كل طلبات التضمين هذا العرض، فإذا أجاب مزوّد بعرض آخر يفشل الطلب بوضوح، بدل أن تُكتب متجهات ترفضها قاعدة البيانات. والانتقال إلى نموذج تضمين بعرض مختلف يعني تعديل هذا الثابت، وكتابة ترحيل يعيد تعريف الأعمدة الثلاثة، وإعادة التضمين، لأن المتجهات ذات الأعراض المختلفة لا يمكن تحويلها؛ لذلك يفرّغ الترحيل قيمها بدل أن يقتطعها.
إذا شغّلت Nessie بجانب تطبيقات أخرى على شبكة Docker مشتركة، فأعطِ حاوية قاعدة البيانات اسمًا مميزًا. اسم خدمة Compose الخاصة بالمشروع هو nessie-postgres ولا يكون أبدًا postgres، لأن اسم الخدمة يصبح اسمًا مستعارًا على شبكة DNS المشتركة، وعندها يتوزع postgres:5432 بالتناوب بين قاعدتي بيانات.
الإعدادات
تأتي إعدادات وقت التشغيل في طبقات: ملف nessie.config.json يُركَّب للقراءة فقط في الـAPI والعامل، وفوقه متغيرات البيئة. والربط بين الاثنين هو ConfigEnvMap في packages/config، وهو القائمة المرجعية.
يحدد NESSIE_MODE شكل النشر: local (الافتراضي، ومسار المطور)، أو hosted، أو selfHosted، وهو ما يضبطه التثبيت الذاتي. يعطّل هذا الوضع تسجيل الدخول التجريبي، ويستلزم قائمة سماح لـCORS.
هذه الإعدادات هي التي لا يبدأ نشر selfHosted بدونها، أو لا يعمل بدونها:
| المتغير | السبب |
|---|---|
DATABASE_URL / NESSIE_DB_URL | سلسلة اتصال Postgres. |
NESSIE_AUTH_SECRET | 32 بايت بالترميز الست عشري (openssl rand -hex 32). يوقّع الجلسات ورموز الإعداد الأولي. يفشل بدء التشغيل بدونه خارج وضع local، ويجب أن تكون له القيمة نفسها على كل نسخة وعبر عمليات النشر، وإلا انتهت كل الجلسات مع كل إصدار. |
NESSIE_ENCRYPTION_KEY_RING, NESSIE_ENCRYPTION_ACTIVE_KEY_VERSION | حلقة مفاتيح بصيغة JSON، والتسمية التي تحدد الجذر النشط، ومطلوبة خارج وضع local. وهذا جذر منفصل عن سر التوقيع، إذ تشفّر الحلقة كل سر دائم تخزّنه Nessie. لا تستخدم أحدهما مكان الآخر. |
NESSIE_STORAGE_PROVIDER | يجب أن تكون القيمة s3. القيمة filesystem هي الافتراضية، وتُرفض في وضعي hosted وselfHosted، فالنشر الذي لا يضبط شيئًا هنا يفشل في البدء، بدل أن يكتب الرفعات على قرص لا تستطيع أي حاوية أخرى قراءته. |
NESSIE_CORS_ORIGINS | أصل لوحة الإدارة. لوحة الإدارة والـAPI أصلان منفصلان، ويجب أن تسمح الـAPI بالأصل الذي تخدمه. |
NESSIE_API_PUBLIC_URL, NESSIE_ADMIN_PUBLIC_URL | تُستخدم لإنشاء عناوين إعادة توجيه OAuth خارج طلب HTTP. وتفشل الـAPI عند وقت الطلب بدون الأول. |
NESSIE_MODEL_PROVIDER, _BASE_URL, _API_KEY | المكان الذي يذهب إليه الاستدلال. راجع أدناه. |
يحمل الملف infrastructure/compose/.env.prod.example الشكل الدقيق لكل هذه الإعدادات، مع كتل اختيارية — تخزين الكائنات، وخزنة الأسرار، وSSO، والبريد المتصل — معلّقة في مواضعها. أما الكتالوج الكامل، بما فيه كل إعداد اختياري، فهو docs/deployment/configuration.md.
النشر باستخدام Docker Compose
انسخ .env.prod.example إلى .env بجوار docker-compose.prod.yml واملأه، ثم شغّل قاعدة البيانات، وابنِ المشروع، ورحّل البيانات، وابدأ الخدمات. الترتيب مهم: تعمل الترحيلات والبيانات الأولية على قاعدة بيانات تعمل بالفعل، قبل أن تقدّم أي خدمة طلبات.
COMPOSE="docker compose -f infrastructure/compose/docker-compose.prod.yml"
bash infrastructure/compose/ensure-encryption-key-ring.sh infrastructure/compose/.env
$COMPOSE up -d nessie-postgres
$COMPOSE build nessie-api nessie-admin nessie-web
$COMPOSE run --rm --no-deps nessie-api pnpm --filter @nessie/api prisma:migrate:deploy
$COMPOSE run --rm --no-deps nessie-api pnpm --filter @nessie/api seed:connectors
$COMPOSE run --rm --no-deps nessie-api pnpm --filter @nessie/api seed:apps
$COMPOSE run --rm --no-deps nessie-api pnpm --filter @nessie/api reconcile
$COMPOSE up -dتشغّل ملفات Dockerfile الخاصة بالإنتاج فحص lint لكل حزمة قبل بنائها، فإخفاق فحص lint يعني إخفاق البناء.
خطوة reconcile لا يمكن تجاوزها
reconcile يزرع قواعد السياسة الافتراضية لكل مؤسسة، ويعيد منح أدوات MCP المحمية وأدوات المساعد الشخصي. كانت الـAPI تفعل ذلك عند الإقلاع على كل نسخة؛ أما الآن فتتصل وتستمع ولا تفعل غير ذلك، لذلك من دون هذه الخطوة لا تملك أول مؤسسة أي قواعد سياسة، ويُرفض كل ربط للوكلاء. وهي آمنة للتكرار، فتشغيلها في كل نشر لا يكلّف شيئًا، ويجب أن تشغّلها أيضًا أي ترقية تُطبَّق يدويًا.
حساب المالك الأول
لا يملك تثبيت selfHosted الجديد أي مستخدمين، لذلك تُصدر الـAPI رمز إعداد أولي يُستخدم مرة واحدة وتسجّل رابط الإعداد. يُخزَّن الرمز في Postgres لا في عملية تشغيل، فتتفق كل النسخ عليه.
docker compose -f infrastructure/compose/docker-compose.prod.yml logs nessie-api 2>&1 | grep bootstrapافتح https://<your-admin-host>/bootstrap?token=<token> وأنشئ حساب المالك. تنتهي صلاحية الرمز بعد 15 دقيقة؛ وأعد تشغيل خدمة الـAPI لإصدار رمز جديد.
إذا أعددت SSO، فلا توجد خطوة إعداد أولي أصلًا. يُلغى وضع الإعداد الأولي فور تفعيل مزوّد مصادقة خارجي، وإلا لاختطف شاشة تسجيل الدخول، ويصبح أول شخص يسجّل الدخول عبر SSO مالكًا للفريق الافتراضي.
ضبط الاستدلال
الدردشة متوافقة مع OpenAI. يمثّل NESSIE_MODEL_BASE_URL نقطة النهاية، وNESSIE_MODEL_API_KEY رمز الحامل، وNESSIE_MODEL_PROVIDER يسمّي المهايئ، فتبني Nessie مهايئات لـopenai وkimi وdeepseek، وتصل إلى أي مزوّد آخر عبر الموصّل العام المتوافق مع OpenAI. يحدد NESSIE_MODEL_NAME النموذج الافتراضي للنشر، ويتقدّم عليه اختيار النموذج الخاص بكل وكيل.
تُوجَّه التضمينات بشكل منفصل، لأن مزوّد الدردشة قد لا يقدّمها أصلًا؛ فـDeepSeek مثلًا لا يملك نقطة نهاية للتضمين. يرث كل حقل NESSIE_EMBEDDING_* غير المضبوط قيمة مزوّد الدردشة، فالنشر الذي لا يضبط أيًّا منها يضمّن عبر نقطة نهاية الدردشة. اضبط NESSIE_EMBEDDING_PROVIDER وNESSIE_EMBEDDING_MODEL، وعلى وكيل يوجّه حسب الخدمة، مقطع الخدمة أيضًا، حين لا يستطيع مزوّد الدردشة التضمين. والخطأ هنا يفشل بصمت لا بوضوح: تنتقل استعادة الذاكرة وبحث قاعدة المعرفة إلى قناة معجمية وتكملان العمل، مع تسجيل kb_search: query embedding failed أثناء ذلك.
لا تتأثر محاسبة الإنفاق في Nessie نفسها، أي سجل الرموز لكل مؤسسة وبوابة الميزانية، بنقطة النهاية التي توجّهها إليها، وتعمل بالطريقة نفسها سواء أُعدّ التوقيع المرتبط بكل استدعاء أم لم يُعدّ.
تسجيل الدخول
تصادق Nessie الأشخاص عبر UnlikeOtherAI، التي تدير أيضًا بنية المؤسسات والفرق. تعرض صفحة تسجيل الدخول في لوحة الإدارة زرًا واحدًا بعنوان «تسجيل الدخول / التسجيل».
التكامل تدفق لتهيئة JWT وليس OIDC القياسي: تقدّم الـAPI وثيقة إعداد موقّعة بخوارزمية RS256 عند GET /api/auth/sso/config، وتقدّم مجموعة المفاتيح JWKS المطابقة عند GET /.well-known/jwks.json، وكلاهما على مضيف الـAPI. والإعداد خطوة واحدة تحدث مرة واحدة: أنشئ زوج مفاتيح RSA-2048 واضبط UOA_CONFIG_JWT_PRIVATE_KEY_B64 وUOA_CONFIG_JWT_KID وUOA_DOMAIN وUOA_CONFIG_URL وUOA_JWKS_URL وUOA_REDIRECT_URL وUOA_CONTACT_EMAIL؛ ثم انقر الزر مرة واحدة، فيُنشأ طلب تكامل؛ ثم وافق عليه، فيُرسَل رابط لمرة واحدة إلى عنوان الاتصال يحمل السر السري للعميل؛ واضبط UOA_CLIENT_SECRET وأعد تشغيل الـAPI. ويجب أن يكون المعرّف kid فريدًا لكل نطاق.
الوصول عبر نطاق، مع TLS
يخدم النشر ثلاثة أسماء: الموقع العام، ولوحة الإدارة، والـAPI. أصل لوحة الإدارة وأصل الـAPI غير قابلين للتبادل. يُدمج Dockerfile.admin القيمة VITE_API_BASE_URL في الحزمة وقت البناء، فيجب أن تُبنى كل نسخة من لوحة الإدارة، بما فيها بناء سطح المكتب الذي يضمّنها، مقابل أصل الـAPI. فإذا بُنيت مقابل أصل لوحة الإدارة نفسها، أصبحت /api/auth/providers صفحة HTML الخاصة باللوحة، وبقيت صفحة تسجيل الدخول عالقة عند «جارٍ تحميل مزوّدي الدخول...».
ينهي Caddy اتصال TLS في الإنتاج، فيحصل على شهادات لكل اسم مضيف عبر HTTP-01، ولا يحتاج إلى إضافة لمزوّد DNS من أجل ذلك. وجّه الوكيل العكسي إلى حاويتي لوحة الإدارة والموقع على المنفذ 80، وإلى الـAPI على منفذها الداخلي.
اضبط NESSIE_API_TRUSTED_PROXY_HOPS على عدد الوكلاء أمام الـAPI، أي 1 خلف وكيل عكسي واحد. وقيمته الافتراضية 0، وهي تتجاهل X-Forwarded-For تمامًا. وهذا قرار واحد له نتيجتان: فهو أيضًا ما يحدد مفاتيح حدود معدل المصادقة؛ فإن كان منخفضًا جدًا شاركت كل الزبائن حصة الوكيل، وإن كان مرتفعًا جدًا استطاع العميل تزوير عنوانه. فضّل القيمة المنخفضة وقِسها.
يمكن تخصيص نطاق فرعي لكل مؤسسة وفريق، وهو متاح ومعطّل افتراضيًا. ويتحكم فيه NESSIE_TEAM_HOST_BASE_DOMAIN؛ فإذا لم يُضبط، لا يُوجَّه المستأجرون حسب اسم المضيف إطلاقًا، وتمنع بوابة إصدار الشهادات NESSIE_TLS_CHECK_KEY كل شيء ما دامت غير مضبوطة، وهذا هو الافتراضي الصحيح لنشر لا يستخدم هذه الميزة.
فحوص الصحة
| النقطة النهائية | السؤال الذي تجيب عنه |
|---|---|
GET /api/health | هل هذه العملية حيّة؟ لا تفشل إلا بعد أن تبدأ الإنهاء. |
GET /api/health/ready | هل يمكن لهذه النسخة أن تستقبل طلبات جديدة؟ هذا هو الفحص الذي يجب أن يوجّه إليه موازن الحمل. |
GET /api/ops/health | كيف يسير النشر؟ تقرير موجّه لشخص: نبضات العمال، وعمق طابور المهام، والمهام المتوقفة، وليس فحصًا آليًا. |
عند SIGTERM تُنهي الـAPI عملها بسلاسة بدل أن تتوقف فورًا. يحدد NESSIE_SHUTDOWN_TIMEOUT_MS مهلة هذا الإنهاء، وقيمته الافتراضية 25 ثانية؛ اجعلها أقل من فترة السماح التي يمنحها المُنسِّق للحاوية، وإلا أنهت البيئة العملية قبل أن يفيد الإنهاء بشيء.
الترقية
الترقية هي تنفيذ prisma migrate deploy على قاعدة البيانات الموجودة، ثم البيانات الأولية، ثم reconcile. وهي الأوامر الأربعة نفسها التي في أول نشر.
يُختبر المسار على الطريق الذي يسلكه المستضيف الذاتي فعلًا، لا على قاعدة بيانات جديدة: تستعيد CI لقطة مأخوذة قبل عشرين ترحيلًا، وترحّلها إلى أحدث إصدار، وتتحقق من المخطط، وتشغّل فحص دخان على الجداول الأساسية. وأي إصدار داخل هذه النافذة الأخيرة مصدر ترقية مُثبت. ومجلدات الترحيل غير قابلة للتغيير بعد إيداعها، ويُفشل خطوة lint البناء إذا أُعيدت تسميتها أو أُعيد ترقيمها أو حُذفت أو عُدّلت، لأن كل ذلك يكسر migrate deploy على قاعدة بيانات سجّلت الصف القديم.
ترحيل واحد فاشل يوقف كل نشر يليه. يتوقف migrate deploy عند أول إخفاق، ويرفض كل ترحيل لاحق برمز P3009، فتبقى التثبيتة على الإصدار القديم إلى أن يزيل أحدهم هذا الحجز يدويًا. وقد أمضى الإنتاج في المشروع يومًا كاملًا يرفض كل نشر بهذه الطريقة. ولإزالة الحجز يجب التأكد من أن الترحيل الفاشل تراجع فعلًا، فلا ينبغي أن توجد أي من الكائنات التي ينشئها، وما يعدّله يجب أن يبقى بشكله الأصلي، ثم تسجيله كمتراجع. والترحيل الذي طُبّق جزئيًا ليس مرشحًا لذلك.
لا يوثّق المستودع النسخ الاحتياطي والاستعادة لنشر Compose الذاتي، لذلك لا تصف هذه الصفحة إجراءً لذلك.
على جهاز واحد، أو في سحابة مُدارة
المسار الموثوق هو نشر Docker Compose على مضيف واحد الموصوف أعلاه، فهو ما يشغّله إنتاج المشروع نفسه، على خادم واحد، مع قاعدة البيانات وتخزين الكائنات والـAPI والعامل ولوحة الإدارة والموقع، كحاويات خلف وكيل عكسي مشترك.
يغيّر التوسع إلى أكثر من جهاز ثلاثة أمور، وكلها مفروضة لا مجرد نصائح. خارج وضع local تعمل الـAPI والعامل كعمليتين منفصلتين، وكل واحدة في عدة نسخ، لذلك تُرفض أي قدرة تفترض أن عملية واحدة تملك قرص الجهاز: تخزين الكائنات على الملفات (فشل عند بدء التشغيل)، وقوالب بيئة التنفيذ مع مزوّد docker (معطّلة، ويفشل التوفير بوضوح)، والأدوات المدمجة file_read وfile_write وfile_glob (نتيجة أداة فاشلة يستطيع الوكيل قراءتها). ويسمّي كل رفض الإعداد والوضع وما يجب استخدامه بدلًا منه.
للسحابة المُدارة يحمل المستودع شجرة Terraform في infrastructure/terraform/ تستهدف Google Cloud: الـAPI كخدمة Cloud Run بحد أدنى نسخة واحدة، لأنها تحتفظ بعميل LISTEN دائم في Postgres وتشغّل عمليات الصيانة الدورية، فتقليصها إلى صفر يوقف التسليم الفوري؛ والعامل كـ Cloud Run worker pool، لأنه لا يفتح منفذًا فلن تصبح الخدمة العادية جاهزة أبدًا؛ ووظيفة ترحيل تتحكم في كل عملية طرح، وCloud SQL لـPostgreSQL 17، ودلو GCS يُوصل إليه عبر الواجهة الخلفية المتوافقة مع S3.
هذا المسار مكتوب، لكنه لم يُجرَّب بعد. يذكر المستودع ذلك صراحة، وتذكره هذه الصفحة كذلك: لا يوجد مشروع Google Cloud لهذا المسار، ولم يُخطَّط أي شيء في الشجرة أو يُطبَّق على مشروع حقيقي، ولم يُختبر مسار الرفع المتعدد الأجزاء إلى GCS على دلو حقيقي، ولم يُتحقق من نوع مورد worker pool، وعدد قفزات الوكيل الموثوق فيه قيمة نائبة ينبغي قياسها، لا قياس فعلي. ولا تُنمذج لوحة الإدارة والموقع العام هنا أصلًا. عامله كنقطة بداية تتحقق منها، لا كنشر مدعوم.
أين تعيش بيانات النشر، وما يحتفظ به البرنامج وما لا يحتفظ به عمدًا، موضوع صفحة منفصلة: صنع في الاتحاد الأوروبي، وأين تُحفظ بياناتك.