API
واجهة HTTP التي تقوم عليها كل تطبيقات Nessie، وكيفية المصادقة عليها.
ما هي واجهة API
لدى Nessie واجهة API واحدة تعتمد على HTTP. تطبيق الويب الإداري وتطبيقات macOS وiOS والخدمة الخلفية للمنفّذ عن بُعد كلها عملاء عاديون لهذه الواجهة، ولا يمكن الوصول إلى أي شيء يستطيعون فعله بطريقة أخرى. وهي واجهة JSON تعمل على خادم Fastify، وليست GraphQL، ولا تملك مجموعة “عامة” منفصلة: فالواجهة التي تستدعيها نصوصك البرمجية هي الواجهة نفسها التي يستدعيها المنتج.
لا يوجد جزء للإصدار في المسار. تقع كل المسارات تحت /api، باستثناء نقطة MCP الواردة POST /mcp وعدد قليل من مستندات الاكتشاف تحت /.well-known/. ويُحافَظ على التوافق بإضافة الحقول فقط، فيستمر العميل الذي يتجاهل ما لا يعرفه في العمل.
عنوان URL الأساسي
تُستضاف Nessie ذاتيًا، لذا فإن عنوان URL الأساسي هو أصل الخادم الذي تقدّم عليه منصتك واجهة API. يضبطه المشغِّل في NESSIE_API_PUBLIC_URL، وهي القيمة نفسها التي تستخدمها واجهة API حين تحتاج إلى إنشاء عنوان URL مطلق خاص بها. وفي النسخة التي نشغّلها، هو https://api.nessie.works؛ أما نسخة التطوير التي تُشغَّل بالأمر pnpm dev فتستجيب على http://localhost:5454.
تطبيق الويب الإداري أصل مختلف عن واجهة API، لذلك تخضع عملاء المتصفح لقائمة سماح CORS (NESSIE_CORS_ORIGINS). أما الطلبات الصادرة من نص برمجي أو خادم فلا أصل لها يُفحص، ولا تتأثر بذلك.
المصادقة
يحمل كل طلب خارج مجموعة صغيرة من المسارات العامة المقصودة رمز وصول في الترويسة القياسية:
Authorization: Bearer <token>الرمز هو JWT موقَّع تصدره واجهة API. وهو قصير العمر: ثلاثون دقيقة افتراضيًا (NESSIE_AUTH_TOKEN_TTL)، وهو بيانات الاعتماد الوحيدة التي يقدّمها أي مستدعٍ عادي. لا تملك Nessie مفاتيح API شخصية طويلة الأمد. وإذا أردت أن يستدعي نص برمجي واجهة API، فإنه يسجّل الدخول بالطريقة نفسها التي يسجّل بها متصفح الشخص، ويجدّد رمزه كلما تقدّم.
الحصول على رمز
POST /api/auth/session ينشئ جلسة. ويُعيد رمز الوصول وهوية المستدعي، ويضع رمز التجديد في ملف تعريف ارتباط منفصل مُقيَّد بـ HTTP فقط.
تعتمد طريقة إثبات هويتك على كيفية نشر النسخة. تقبل النسخة التي تعمل بوضع local البريد الإلكتروني وكلمة المرور. أما في نسخة hosted أو selfHosted فيُرفض تسجيل الدخول بكلمة المرور صراحةً برمز 403 PASSWORD_AUTH_DISABLED، ولا منفذ للدخول إلا عبر مزوّد الهوية المُعدّ، فلا يمكن أن ينشأ للحساب مسار هوية ثانٍ وأضعف إلى جانبه.
تسجيل الدخول عبر مزوّد هوية
تسجيل الدخول الموحّد هو تبادل رمز التفويض في OAuth مع PKCE، على ثلاث خطوات. ومزوّد الهوية في Nessie هو UnlikeOtherAI، وهو الذي يملك أيضًا بنية المؤسسات والفرق التي تعكسها النسخة. والتبادل أدناه بالشكل نفسه مع أي مزوّد مُعدّ.
GET /api/auth/providersيسرد ما تقبله هذه النسخة، ولكل مزوّدproviderIdوتسمية وبيان عما إذا كان ينبغي لصفحة تسجيل الدخول أن تحوّل إليه تلقائيًا.GET /api/auth/providers/:providerId/authorize، معcodeChallengeوredirectUriوstateمعاملات في الاستعلام، يُعيد{ data: { authorizeUrl } }. وجِّه الشخص إليه.- يعيد المزوّد الشخص إلى
redirectUriلديك مع رمز. أرسِل هذا الرمز إلىPOST /api/auth/sessionمع{ providerId, code, codeVerifier, redirectUri }؛ فالحقول الأربعة مطلوبة معًا، والمجموعة الناقصة تُقابَل بالرمز400 EXTERNAL_AUTH_INCOMPLETE.
تجديد الجلسة
لا يظهر رمز التجديد أبدًا في متن الاستجابة. بل يُضبط كملف تعريف ارتباط HTTP-only اسمه nessie_refresh، ونطاقه المسار /api/auth، ومدة صلاحيته ثلاثون يومًا افتراضيًا (NESSIE_AUTH_REFRESH_TOKEN_TTL). ويؤدي استدعاء POST /api/auth/refresh مع ملف تعريف الارتباط هذا إلى استهلاكه، وإصدار رمز وصول جديد، وضبط ملف تعريف ارتباط جديد مكانه. وتتدوّر الرموز، لذلك لا يصلح الرمز المسروق إلا إلى أن يجدّد العميل الحقيقي جلسته في المرة التالية.
يحتفظ المتصفح المسجَّل الدخول إلى عدة حسابات بملف تعريف ارتباط من هذا النوع لكل تسجيل دخول، باسم nessie_refresh_<slot>، ويحدد الحساب المقصود بالطلب بترويسة x-nessie-account-slot. والعميل الذي لا يرسل هذه الترويسة يقصد nessie_refresh، وهذا كل ما يحتاجه عميل سطر الأوامر.
يحتاج عميل سطر الأوامر إذًا إلى مخزن لملفات تعريف الارتباط. ومع curl يكون ذلك بالخيار --cookie-jar في استدعاء تسجيل الدخول، والخيار --cookie في التجديد، كما في المثال التطبيقي أدناه.
ما الذي يُتحقَّق منه في كل طلب
المصادقة ليست مجرد فحص للتوقيع. قبل تشغيل أي معالج، تتحقق واجهة API من توقيع الرمز وانتهاء صلاحيته، وتؤكد أن الجلسة لم تُلغَ (سواء عبر إصدار الرمز على مستوى المستخدم أو عبر معرّف الجلسة الفردي)، وتحمّل عضوية المستدعي في المؤسسة التي يسمّيها الرمز. والعضوية المعطّلة تعني 403. وعندما تكون المؤسسة مرتبطة بمزوّد هوية خارجي، تُعاد مراجعة وضع المستدعي الحالي لدى ذلك المزوّد عند الطلب، ولا يُقرأ من الرمز، فالوصول الذي سُحب في المصدر يتوقف فورًا لا عند انتهاء الصلاحية التالي.
بيانات اعتماد ليست رموز جلسات
توجد بيانات اعتماد حاملة أخرى من نوعين، وليس أيٌّ منهما مفتاحًا عامًا. تُقبل بيانات اعتماد جهاز الصوت في مسارات المكالمات الصوتية فقط، وتُقبل بيانات اعتماد وصول الوكيل في نقطة MCP فقط. وتقديم أيٍّ منهما في أي مكان آخر يُرفض برمز 403 يسمّي بيانات الاعتماد، ولا يُعاد تجربته كجلسة أضعف. راجع MCP والأدوات المتصلة لمعرفة كيفية إصدار بيانات اعتماد الوكيل.
غلاف الاستجابة
كل استجابة ناجحة كائن يحتوي على المفتاح data. لا توجد مصفوفة مجرّدة ولا حمولة على المستوى الجذري، فلا يحتاج العميل أبدًا إلى التمييز بين الأشكال:
{
"data": { "id": "3f1c…", "label": "engineering" }
}تضيف نقاط نهاية القوائم كائن meta بجوار البيانات. ولا يُضاف أي شيء آخر على المستوى الأعلى أبدًا.
تستخدم الإخفاقات شكلًا واحدًا أيضًا، أيًّا كان مصدرها:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "A message needs content or at least one attachment",
"field": "body",
"details": { "formErrors": [], "fieldErrors": {} }
}
}code هو الجزء القابل للقراءة آليًا، وهو ما ينبغي أن تتفرّع عليه الشيفرة؛ وmessage موجّهة إلى الإنسان. ويَرِد field وdetails عندما يكون الإخفاق محددًا بما يكفي لتحديد موقعه. أما خطأ الخادم غير المعالَج فهو دائمًا 500 INTERNAL_ERROR برسالة ثابتة، إذ يُسجَّل نص الخطأ الأصلي ولا يُعاد أبدًا، لأنه ربما التقط بيانات الطلب في طريقه إلى الخارج.
رموز الأخطاء التي ستواجهها أولًا
| الحالة | الرمز | المعنى |
|---|---|---|
| 400 | VALIDATION_ERROR | لم يطابق الجسم أو الاستعلام المخطط المحدد. |
| 401 | AUTH_REQUIRED | لا يوجد رمز حامل، أو أن الترويسة مشوّهة. |
| 401 | TOKEN_EXPIRED | انتهت صلاحية رمز الوصول. جدّده ثم أعد المحاولة. |
| 401 | TOKEN_REVOKED | سُجّل الخروج من الجلسة، أو أُجبرت كل الجلسات على الخروج. |
| 403 | ACCOUNT_DEACTIVATED | عضوية هذه المؤسسة عُطّلت. |
| 404 | NOT_FOUND | لا يوجد مسار بهذا الاسم، أو لا يوجد سجل بهذا الاسم يراه هذا المستدعي. |
| 429 | RATE_LIMITED | امتلأ حاجز الطلبات. اقرأ Retry-After. |
| 500 | INTERNAL_ERROR | خطأ خادم غير معالَج، وهو مُبهم عمدًا. |
ترقيم الصفحات
تُقسَّم نقاط نهاية القوائم إلى صفحات بمؤشر keyset مُبهَم، لا باستخدام offset، لأن الصفوف تُضاف بينما يقرأ أحدهم، ومع الإزاحة تكرّر الصفحة الثانية بصمت سجلًا عرضته الصفحة الأولى بالفعل.
تُقبل ثلاثة معاملات في الاستعلام: cursor وlimit وdirection (forward أو backward). الصفحة الافتراضية 25 عنصرًا، والحد الأقصى 100؛ والقيمة الأكبر تُقصّ إلى الحد ولا تُرفض. وتضيف نقاط النهاية التي ترتّب أو تُصفّي أيضًا sort وorder وq للبحث النصي الحر.
يخبرك meta الصفحة بمكانك:
{
"data": [ … ],
"meta": {
"hasMore": true,
"nextCursor": "2026-09-12T08:31:44.005Z|8b2f…",
"prevCursor": null,
"total": 134
}
}nextCursor وprevCursor حقيقيان معًا، فـ“السابق” و“التالي” زرّان يتوقعهما الشخص. وهما مُبهمان: فالخادم يضع مفتاح ترتيبه داخلهما، والعميل الذي يحلّلهما أو ينشئهما سينكسر في اللحظة التي تغيّر فيها القائمة ترتيبها. وtotal موجودة في معظم القوائم، وتُحذف حيث لا يكون العدّ ذا معنى، كنتائج البحث المرتبة.
حدود الطلبات
تُطبَّق الحدود على نوافذ زمنية ثابتة، وتُفرض قبل قراءة جسم الطلب أصلًا. والطلب المرفوض هو 429 برمز RATE_LIMITED وترويسة Retry-After بالثواني.
تُربط معظم الحدود بعنوان IP للعميل، أما الحدود الحساسة لبيانات الاعتماد (تسجيل الدخول والتجديد وإعادة إثبات كلمة المرور وكتابة بيانات اعتماد MCP) فتحتفظ بعدّاد لكل حساب إضافةً إلى ذلك، وترفض الطلب عندما يتجاوز أيٌّ منهما الحد. فعنوان واحد يجرّب كلمات مرور على كثير من الحسابات يصطدم بجدار، وحساب واحد يُهاجَم من عناوين كثيرة يصطدم بجدار أيضًا. ويبلغ الحد الافتراضي لتسجيل الدخول 10 محاولات لكل IP و5 لكل بريد إلكتروني كل عشر دقائق؛ وإرسال رسالة 60 في الدقيقة؛ والكتابة تحت /api/agents 60 في الدقيقة. وكل عتبة قابلة للضبط لكل نشر، فاعتبر هذه القيم الافتراضية المشحونة مع المنتج، لا عقدًا ملزمًا. وتُستثنى فحوص الصحة وبثوث الوقت الفعلي.
الوقت الفعلي
الاستطلاع الدوري لقائمة هو الطريقة الخاطئة لمتابعة محادثة. لذلك توجد ثلاث نقاط نهاية للبث بدلًا منه:
GET /api/events/stream— أحداث يرسلها الخادم (server-sent events) لكل ما يستطيع الشخص المسجّل الدخول رؤيته.GET /api/threads/:threadId/stream— أحداث بث لخيط واحد. ويحترمLast-Event-ID، فإعادة الاتصال تستأنف ولا تبدأ من جديد.GET /api/activity— اتصال WebSocket يحمل خلاصة النشاط.
لا تستطيع المتصفحات ضبط الترويسات عند مصافحة WebSocket، لذلك يقبل مسار WebSocket رمز الوصول أيضًا كمعامل في الاستعلام ?token=. وهذا هو المكان الوحيد الذي ينبغي أن يظهر فيه الرمز في عنوان URL؛ وكل مسار آخر يأخذه من الترويسة.
عائلات الموارد
توجد أكثر من ألف نقطة نهاية، وهي أكثر بكثير من أن تُسرد هنا. وهي مُجمّعة في عائلات، وما إن تعرف بادئة العائلة حتى تصبح المسارات الفردية متوقعة: مجموعة عند البادئة، وسجل تحتها، وإجراءات مسمّاة كمسارات فرعية بطريقة POST.
| العائلة | البادئة | ما تغطيه |
|---|---|---|
| المصادقة | /api/auth | تسجيل الدخول والتجديد وتسجيل الخروج والهوية الحالية، وجلسات الشخص الخاصة به. |
| القنوات والخيوط | /api/channels, /api/threads | نموذج المحادثة: القنوات تحتوي على خيوط، والخيوط تحتوي على رسائل، والردود تتفرّع تحت رسالتها الأصلية. |
| الوكلاء | /api/agents | تعريفات الوكلاء، وارتباطات قنواتهم، ومحادثاتهم، ومهامهم، وحالتهم. |
| المشاريع واللوحات | /api/projects, /api/tasks, /api/iterations | تتبّع العمل: لوحات وأعمدة تحت مشروع، ومهام عليها، وتكرارات فوقها. |
| المعرفة | /api/knowledge-base | مساحات من الصفحات والملفات المُنقّحة، مع التعليقات والروابط الخلفية والبحث. |
| البريد | /api/mail, /api/mailbox-connections | صناديق البريد المتصلة، وصناديق بريد الوكلاء، والمسودات التي كتبها وكيل ليوافق عليها شخص. |
| الموافقات | /api/approvals | قائمة الإجراءات التي لا يحق للوكيل تنفيذها من تلقاء نفسه، والاستدعاء الواحد الذي يحسمها. |
| التدقيق | /api/audit-log | سجل التدقيق المربوط بسلسلة تجزئة للمؤسسة، مع نقطة تحقق. للمالكين والمسؤولين. |
| التكلفة والاستخدام | /api/ledger, /api/billing | محاسبة الرموز والتكلفة، والميزانيات وتوقيتات التشغيل؛ وبشكل منفصل، الأرصدة والشحنات. للمالكين والمسؤولين. |
| المؤسسة والفرق | /api/organizations, /api/teams, /api/users | إدارة المؤسسة والفرق والعضوية، بما فيها الدعوات. |
| المحفّزات | /api/triggers | الجداول الزمنية وخطافات الويب التي تبدأ عمل الوكلاء، مع سجل مرات تشغيلها. |
| المكالمات والصوت | /api/calls, /api/voice | الاتصال داخل القناة، وجلسة مساعد الصوت ونصّها وسجلها، ورموز الأجهزة الصوتية. |
| المنفّذون | /api/executors, /api/execution-environment-templates | الأجهزة التي يعمل عليها الوكلاء، وبيئاتها، وقناة إقران الخدمة الخلفية. |
| التشغيلات | /api/runs | التشغيلات الحية للوكلاء: الإلغاء والمتابعة وإعادة التشغيل والربط بمنفّذ. |
| الرفع | /api/uploads, /api/attachments | ارفع الملف أولًا، ثم اربط المعرّف المُعاد بالرسالة. |
| البحث | /api/messages/search | البحث في الرسائل على مستوى المؤسسة كلها، أو مقصورًا على قناة واحدة. |
| الأسرار والإعدادات | /api/secrets, /api/settings/scoped | تخزين بيانات الاعتماد للكتابة فقط مع منح صريحة، وقيم الإعدادات المتدرجة. |
| الأدوات المتصلة | /api/mcp | خوادم MCP التي تتصل بها هذه النسخة لاستدعائها. راجع MCP والأدوات المتصلة. |
| الصحة | /api/health | فحوص الحيوية والجاهزية، وكلاهما عام. ويتحقق /api/health/ready أيضًا من قاعدة البيانات. |
تُفرض الصلاحيات لكل مسار، لا لكل عائلة. وبوجه عام: سجل التدقيق وسجل التكاليف وقواعد السياسات ومستوى التحكم بالاستدلال أسطح خاصة بمالكي المؤسسة ومسؤوليها، بينما المحادثات والمشاريع والمعرفة والموافقات مفتوحة لأي عضو، ويُضيَّق نطاقها بما يستطيع ذلك العضو رؤيته فعلًا.
أمثلة تطبيقية
سجّل الدخول واحتفظ بملف تعريف ارتباط التجديد
على نسخة تعمل بوضع local، وبكلمة مرور. يعود رمز الوصول في متن الاستجابة، ويستقر رمز التجديد في مخزن ملفات تعريف الارتباط.
curl -sS https://api.example.com/api/auth/session \
--cookie-jar nessie.cookies \
-H 'Content-Type: application/json' \
-d '{"email":"ada@example.com","password":"…"}'{
"data": {
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9…",
"me": {
"user": {
"id": "1d4e…",
"email": "ada@example.com",
"displayName": "Ada Lovelace",
"roleIds": ["owner"],
"superAdmin": false
},
"session": {
"sessionId": "9c07…",
"issuedAt": "2026-09-16T09:00:00.000Z",
"expiresAt": "2026-09-16T09:30:00.000Z"
},
"context": {
"organizationId": "5a81…",
"projectId": "7b12…",
"teamId": "2e60…",
"channelId": null,
"bootstrapMode": false
},
"auth": {
"providerId": "local",
"providerType": "local-bootstrap",
"autoRedirectToSso": false
}
}
}
}عندما ينتهي الرمز، استبدل ملف تعريف الارتباط برمز جديد:
curl -sS -X POST https://api.example.com/api/auth/refresh \
--cookie nessie.cookies --cookie-jar nessie.cookiesتأتي الاستجابة بالشكل نفسه { token, me }، ويحمل مخزن ملفات تعريف الارتباط الآن رمز التجديد المُدوَّر.
اعرض القنوات التي تراها
curl -sS https://api.example.com/api/channels \
-H "Authorization: Bearer $TOKEN"{
"data": [
{
"id": "8b2f…",
"label": "engineering",
"type": "standard",
"visibility": "public",
"projectId": "…",
"projectName": "Platform",
"teamId": "…",
"defaultThreadId": "c41a…",
"unreadCount": 3,
"lastMessageAt": "2026-09-15T18:22:07.441Z",
"viewerCanManage": true
}
]
}أضف ?teamId=… لتقصرها على فريق واحد، أو ?includeArchived=true لتضمين القنوات المؤرشفة. وdefaultThreadId هو الخيط الذي تنشر فيه بعد ذلك.
انشر رسالة
تحتاج الرسالة إلى بعض content أو مرفق واحد مرفوع على الأقل. أرسل Idempotency-Key، فالطلب المُعاد يُحسم إلى الرسالة التي أنشأتها المحاولة الأولى، بدلًا من نشر نسخة ثانية.
curl -sS https://api.example.com/api/threads/c41a…/messages \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: 0f9c2d16-8f4a-4c1e-9a77-6b0d4f2e1a53' \
-d '{"content":"Deploying the migration now."}'الرسالة الجديدة تُعاد بالرمز 201:
{
"data": {
"message": {
"id": "d70e…",
"threadId": "c41a…",
"role": "user",
"content": "Deploying the migration now.",
"createdAt": "2026-09-16T09:04:11.882Z"
},
"pendingAgentInvites": []
}
}إعادة المحاولة بمفتاح Idempotency نفسه تُجيب بالرمز 200 مع الرسالة الأصلية ومصفوفة pendingAgentInvites فارغة، لأن المحاولة الأولى قامت بالتنبيه والإرسال اللذين تصفهما المصفوفة.
يستحق رفضان على هذا المسار معالجة صريحة. المحتوى الذي يتجاوز حد طول الدردشة يُقابَل بالرمز 413 MESSAGE_TOO_LARGE، والاستجابة الصحيحة هي رفع النص كملف. والمحتوى الذي يبدو بنيويًا كبيانات اعتماد يُقابَل بالرمز 422 SECRET_INTERCEPTED، إذ يرفض الخادم الرسالة قبل تخزينها أو إرسالها أو تسجيلها، وينبغي أن توضع بيانات الاعتماد في /api/secrets.