# دليل المساهمة في الإضافات يقوم نظام إضافات Voyager على الإضافات التصريحية (Declarative): تصف `plugin.json` بيانات الإضافة الوصفية وعمليات DOM، ثم تصف CSS التغييرات البصرية. لا تُشغّل الإضافات JavaScript عن بُعد؛ بل يُفسّر محرك الإضافات المدمج في Voyager ملف manifest والأنماط. هذا يجعل الإضافات أسهل في المراجعة والصيانة. إذا أردت المساهمة بإضافة، فابدأ من هنا. ## لاستخدامك الشخصي: الإضافات المحلية الإضافة التي تحتاجها في متصفحك وحدك لا تحتاج إلى هذه العملية. أنشئها كإضافة محلية واستوردها من النافذة المنبثقة: دون Issue ودون PR ودون نسخ المستودع. يشرح [اكتب إضافتك الخاصة](./write-a-plugin.md) الطرق الثلاث لإنشاء إضافة، بما في ذلك أداة الفحص `create-voyager-plugin` والمهارة الخاصة بوكلاء البرمجة. ## المسار الموصى به 1. تحقّق أولًا من أن الفكرة تناسب الإضافة: عرض القراءة، وإصلاحات التخطيط، وتعديلات السمة، وإخفاء عناصر الصفحة أو تمييزها، والتكييفات البسيطة للمواقع، كلها عادةً مرشحة جيدة. 2. افتح Issue في مستودع Voyager أولًا. اشرح المشكلة والموقع المستهدف والفرق عن الإضافات الموجودة، وانتظر موافقة صريحة من المشرفين قبل البرمجة أو فتح PR. 3. استخدم `plugin.json` للبيانات الوصفية ومطابقات المواقع والإعدادات والمساهمات. 4. ضع الأنماط في `style.css` داخل مجلد الإضافة نفسه، ثم أشِر إليه من `contributes.styles`. 5. اختبر محليًا، وأرفق في PR صفحات اختبار أو لقطات شاشة أو تسجيلًا قصيرًا. سيقرر المشرفون ما إذا كانت الإضافة جاهزة للدخول في الكتالوج الرسمي. ## بنية المجلدات توجد الإضافات الرسمية المرفقة مع Voyager في `src/features/plugins/catalog/`، بمجلد واحد لكل منصة: ``` src/features/plugins/catalog/ marketplace.json الفهرس الذي يقرؤه متجر الإضافات في التوثيق sites//site.json محوّل الموقع، ككتلة بيانات sites//plugins//plugin.json إضافة تصريحية sites//plugins//style.css أنماطها sites//plugins//README.md ما الذي تصلحه ولماذا ``` الاكتشاف تلقائي: يعثر `catalog/sites/index.ts` على كل ملف `site.json` وكل ملف `plugin.json` عبر `import.meta.glob`، لذا تكفي إضافة الملفات لإضافة موقع أو إضافة جديدة. لا يوجد جدول ربط تحتاج إلى تعديله. أما `marketplace.json` فليس جدول ربط، بل هو الفهرس الذي يقرؤه متجر الإضافات في التوثيق، ويتحقق اختبار من تطابقه مع نتائج الاكتشاف؛ لذلك تحتاج الإضافة الجديدة إلى مدخل فيه أيضًا. وتكون قيمة `source` مسارًا نسبيًا من مجلد catalog، مثل `sites/deepseek/plugins/reading-width/plugin.json`. والإضافات الموجودة تحت `sites/gemini/` و`sites/aistudio/` استثناء: فهي مرفقة داخل Voyager ولا تظهر في `marketplace.json`. يجب أن تبقى `matches` الخاصة بالإضافة ضمن `matches` الموقع الذي تنتمي إليه، والإضافة التي تخرج عن موقعها تُفشل عملية البناء. ## مفاتيح المحددات الدلالية يربط `site.json` مجموعة ثابتة من المفاتيح الدلالية بمحددات CSS خاصة بالموقع. تُعرَّف المجموعة في `src/features/plugins/sites/semanticKeys.ts`؛ ولا يجوز لملف `site.json` أن يستخدم إلا المفاتيح الواردة في هذه القائمة، وأي مفتاح غير معروف يُرفض. - `userTurn`: حاوية رسالة المستخدم. - `assistantTurn`: حاوية رسالة المساعد. - `thinkingBlock`: قسم التفكير داخل رد المساعد. - `codeBlock`: كتلة كود معروضة. - `composer`: حقل الإدخال الذي يكتب فيه المستخدم طلبه. - `sidebar`: قائمة المحادثات أو شريط التنقل. - `sidePanel`: لوحة ثانوية، مثل artifacts أو canvas. - `headerActions`: مجموعة الإجراءات في أعلى يمين عرض المحادثة. - `scrollContainer`: العنصر الذي يُمرَّر فيه سجل المحادثة. - `composerRoot`: الحاوية التي تضم حقل الإدخال وأزرار إجراءاته. - `sendButton`: زر إرسال الطلب؛ بعض المواقع لا تُظهره إلا بعد أن يصبح الحقل غير فارغ. يعلن الموقع المفاتيح التي يستطيع توفيرها فعلًا فقط. وتشير الإضافة إلى المفتاح بدلًا من المحدد الخام، عبر كتابة `{ "kind": "semantic", "key": "userTurn" }` كقيمة `target` في عملية DOM؛ وبذلك يصبح تغيير تصميم الموقع إصلاحًا في ملف `site.json` واحد فقط. أما `conversationIdPattern` فحقل منفصل في `site.json` وليس محددًا: هو تعبير نمطي على مسار الرابط، ومجموعة الالتقاط الأولى فيه هي معرّف المحادثة، مثل `^/chat/([^/?#]+)`. ## إضافة منصة جديدة إضافة موقع دردشة جديد تتضمن طبقتين منفصلتين: 1. **محوّل الموقع** `catalog/sites//site.json`، ويعلّم Voyager كيف يتعرف على الموقع ويجد رسائل المستخدم ورسائل المساعد وحقل الإدخال والشريط الجانبي وعلامات السمة. 2. **إضافة** ضمن `catalog/sites//plugins//`، تحل مشكلة واحدة يواجهها المستخدم على ذلك الموقع، مثل عرض القراءة أو إصلاح عرض معين. يحمل `site.json` الحقول `id` (ويساوي اسم مجلده)، و`label`، و`matches`، و`selectors`، و`theme`، و`brandColor`، و`capabilities`، إضافةً إلى الحقلين الاختياريين `conversationIdPattern` و`turnKeyAttributes`. يسمّي `turnKeyAttributes` السمات التي تحفظ هوية رسالة المستخدم عندما تعيد الصفحة عرضها؛ ولا يعمل تتبع الإرسال (الذي يستخدمه عرض النشاط في المجلدات وطوابع الوقت في الجدول الزمني) إلا على موقع يحدد هذه السمات، إلى جانب `composer` و`userTurn` و`conversationIdPattern`. يتحقق `validateSiteAdapterData` في `src/features/plugins/sites/siteAdapterData.ts` من صحة الملف، ويتحقق `bun run catalog:build` من كل موقع وكل إضافة قبل النشر. كل منصة إضافات معطاة كبيانات. والمواقع المدعومة حاليًا هي ChatGPT وClaude وDeepSeek. أما Gemini وAI Studio فهما سطحان أصليان في Voyager، ولكل منهما `site.json` خاص (`catalog/sites/gemini/` و`catalog/sites/aistudio/`)، لكنهما يُشحنان داخل الحزمة ولا يُنشران أبدًا في الكتالوج البعيد. والمحولات الخمسة كلها، وهي `adapters/gemini.ts` و`aistudio.ts` و`claude.ts` و`chatgpt.ts` و`deepseek.ts`، عبارة عن أغلفة من سطر واحد فوق `site.json`: عدّل ملف JSON، لا ملف TypeScript. يحمل الكتالوج المنشور لكل مضيف بيانات الموقع أيضًا، لذلك يصل إصلاح المحددات في `site.json` إلى المستخدمين دون الحاجة إلى إصدار جديد من الإضافة. لكن الموقع الجديد تمامًا يحتاج إلى محوّل من الأساس، لأنه يحتاج أيضًا إلى إذن المضيف وتسجيل سكريبتات المحتوى، وهذان يُشحنان داخل الحزمة. في حالة منصة جديدة مثل DeepSeek، اجعل عمل المحوّل مركّزًا على عقد المنصة. ضع `site.json` واختبارات المحددات المطلوبة والحد الأدنى من التوثيق الذي يشرح الدعم في تغيير واحد متكامل. يلتقط سجل المواقع المحوّل الجديد من الملف نفسه، فلا توجد قائمة تسجيل تحتاج إلى تعديل. ولا تُضِف موقعًا جديدًا لكل إضافة. بعد قبول المحوّل، يمكن تقديم الإضافات اللاحقة كتغييرات منفصلة ومركّزة. وإذا كانت التغييرات مترابطة فيما بينها، فسلسلة PR مكدّسة (Stacked PR) مفيدة: يُبنى كل فرع لاحق على الفرع السابق، ويعرض كل PR تغييره التدريجي فقط. ويجب أن تلتزم السلسلة بقاعدة النطاق نفسها: هدف واضح واحد للمستخدم في كل PR، مع اختباراته المطلوبة وتنظيفه. لا يتطلب دعم الموقع تكاملًا مع واجهة نموذج (Model API). لا تضِف سكريبتات محتوى ثابتة ولا أذونات مضيف مطلوبة لمنصة جديدة؛ بل استخدم مسار الأذونات الاختيارية والتسجيل الديناميكي للمواقع الخاصة بالإضافات فقط. يعتمد مسار الأذونات الاختيارية والتسجيل الديناميكي على إصدار المتصفح. يدعم Chrome وEdge المسار الذي تستخدمه الإضافة. ويتطلب Firefox الإصدار 128 أو أحدث للأذونات الاختيارية لمضيفات الويب، ويتطلب Safari الإصدار 16.4 أو أحدث للتسجيل الديناميكي لسكريبتات المحتوى. وفي إصدارات المتصفح المدعومة الأقدم، قد تمتنع النافذة المنبثقة بشكل صحيح عن تفعيل إضافة لموقع مخصص، لأن الواجهات المطلوبة لا توفر منحًا دائمًا. فلا تتجاوز ذلك بإضافة إذن مضيف واسع وثابت دون موافقة المشرفين. ## نطاق الإضافة يجب أن يُحدَّد نطاق الإضافة بالمشكلة التي يحلّها للمستخدم، لا أن يُقسَّم آليًا حسب المنصة. تعيش الإضافة من الكتالوج داخل مجلد موقع واحد، ويجب أن تكون كل قيم `matches` الخاصة بها مغطاة بملف `site.json` لذلك المجلد (وإلا يفشل `bun run plugin:check`). لذلك لا يمكن لإضافة واحدة أن تغطي Claude وChatGPT معًا. وإذا كانت الميزة نفسها تناسب عدة منصات، مثل عرض القراءة أو التنقل بين الصفحات أو تخطيط كتل الكود، فاشحن إضافة واحدة في مجلد كل موقع (مثلًا `sites/claude/plugins/reading-width` و`sites/chatgpt/plugins/reading-width`)، وأبقِ أسماءها وإعداداتها ووصفها متسقة. وإذا احتاجت كل منصة إلى إعدادات أو منطق DOM أو نصوص واجهة مختلفة جدًا، فالفصل بين الإضافات أوضح. لا تضع سلوكيات غير مترابطة في إضافة واحدة لمجرد أن تكون "تغطي كل شيء"؛ فالإضافة الجيدة تحل مشكلة واحدة واضحة. قاعدة سريعة: - الهدف نفسه للمستخدم، والإعدادات نفسها، والمحددات فقط تختلف: إضافة واحدة لكل مجلد موقع، مع أسماء وإعدادات متسقة. - السمة نفسها، لكن سلوك المنصات يختلف كثيرًا: افصلها، مع إبقاء الأسماء والأوصاف مترابطة. - الأهداف مختلفة: لا تدمجها. ## تجنّب الإضافات المكررة قبل الإرسال، راجع متجر الإضافات والإضافات الرسمية الموجودة. إذا كانت هناك إضافة جيدة بالفعل، فحسّنها بدلًا من إنشاء إضافة مشابهة. لا تستحق الإضافة المكررة القبول إلا إذا قدّمت تحسينًا واضحًا، مثل: - دعم منصة مهمة لا تغطيها الإضافة الأصلية. - إصلاح مشكلة توافق لا تستطيع الإضافة الأصلية حلها. - أداء أو إمكانية وصول أو قابلية صيانة أفضل بوضوح. - تجربة مستخدم مختلفة وذات معنى، لا مجرد اسم جديد أو تعديل بسيط في النمط. وبذلك يبقى المتجر نظيفًا، ويسهل على المستخدمين الاختيار. ## مثال بسيط لا تبدأ من مجلد فارغ: الأمر `bun run plugin:new --site ` ينشئ `catalog/sites//plugins//` مع `plugin.json` و`style.css` و`README.md`، ثم يضيف المدخل المناسب إلى `marketplace.json` نيابةً عنك. ما يُنشئه الأمر نقطة بداية وليس موافقة. وشكل ملف manifest الذي تُكمله هو كالتالي: ```json { "$schema": "https://voyager.nagi.fun/plugin.schema.json", "id": "your-name.example-plugin", "name": "Example Plugin", "version": "1.0.0", "description": "A short description of what this plugin improves.", "author": "your-name", "category": "readability", "license": "MIT", "engine": ">=1.0.0", "tier": "declarative", "matches": ["https://claude.ai/*"], "contributes": { "styles": [{ "file": "style.css" }], "domOps": [ { "op": "addClass", "target": "body", "className": "gv-plugin-example" } ] } } ``` سطر `$schema` اختياري، وهو موجَّه إلى محرّرك فقط: يشير إلى مخطط JSON الخاص بملف manifest، فيتحقق المحرّر من الملف ويكمله أثناء الكتابة. ولملف `site.json` مخطط خاص به على `https://voyager.nagi.fun/site.schema.json`. يمكن كتابة `style.css` كأي CSS عادي، لكن يجب أن تكون أنماط الإضافة محصورة تحت فئة خاصة بك من النوع `gv-plugin-*`: ```css .gv-plugin-example .some-target { max-width: 880px; } ``` ## ملاحظات على manifest - استخدم بادئة للمؤلف أو صيغة نطاق عكسي في `id`، مثل `your-name.reading-width`، لتفادي التعارض. - اجعل `matches` محدودة قدر الإمكان. طابق فقط المواقع التي تحتاج الإضافة إلى العمل فيها فعلًا. - لا تُدرج الإضافة عدة مضيفات في `matches` إلا إذا كان `site.json` الخاص بموقعها يغطيها كلها (مثل chatgpt.com وchat.openai.com). ولتغطية موقع آخر، أضف إضافة في مجلد ذلك الموقع. - القيم الموصى بها لـ `category` هي: `render-fix` أو `theme` أو `layout` أو `readability` أو `productivity` أو `integration` أو `other`. - اضبط `engine` على نسخة محرك الإضافات التي تحتاجها. ويمكنك الاستعانة بالإضافات الرسمية كأمثلة. - أضف `i18n` للصينية والإنجليزية واللغات الشائعة الأخرى كلما أمكن ذلك. ## قيود CSS والموارد تُعامَل الإضافات التصريحية كمدخلات غير موثوقة، لذا اجعل مواردها مكتفية بذاتها: - لا تستخدم `@import`. - لا تشِر إلى صور بعيدة أو خطوط خارجية أو CSS بعيد. - يمكنك استخدام CSS عادي، والخصائص المخصصة (Custom Properties)، واستبدالات قيم الإعدادات في Voyager. - ابدأ أسماء فئات الإضافة بالبادئة `gv-plugin-` لمنع تسرّب الأنماط إلى الموقع المضيف أو إلى Voyager نفسه. إذا احتاجت إضافتك إلى إعدادات، فابدأ بالإعدادات الرقمية متى أمكن. مثلًا، يمكن لإضافة عرض القراءة أن تكتب قيمة الإعداد في متغير CSS ثم يستهلكها CSS. ## حدود عمليات DOM تدعم الإضافات التصريحية حاليًا: - `addClass`: إضافة فئة إلى العناصر المستهدفة. - `setAttribute`: ضبط سمة. - `setStyle`: ضبط أنماط inline أو متغيرات CSS. أنهِ القيمة بـ `!important` لتطبيقها بتلك الأولوية؛ إذ تتجاهل إصدارات Voyager الأقدم هذه القيمة، لذا اضبط `engine` على `>=1.9.0` إذا كانت إضافتك تعتمد عليها. وتُتتبَّع الاختصارات مثل `margin` لكل خاصية فرعية على حدة، فإيقاف إضافة واحدة لا يمسح `margin-top` الخاص بإضافة أخرى. - `hide`: إخفاء العناصر المستهدفة. يمكن أن يكون الهدف محددًا من محددات CSS أو أحد المفاتيح الدلالية المذكورة أعلاه، مكتوبًا بالشكل `{ "kind": "semantic", "key": "userTurn" }`. المفاتيح الدلالية أكثر ثباتًا عادةً، لكنها تتطلب أن يعلن محوّل الموقع الحالي ذلك المفتاح. يجب أن تكون العمليات التصريحية قابلة للتراجع، وآمنة عند تكرار تنفيذها. لا تعتمد على حالة صفحة تحدث مرة واحدة، ولا تفترض أن DOM لن يتغير. ### البدائيات (Primitives) بعض السلوكيات لا يمكن وصفها بـ CSS أو بتعديلات DOM قابلة للتراجع. والبدائية (Primitive) هي شيفرة من الطرف الأول مرفقة داخل Voyager، ويمكن لملف manifest استدعاؤها بالاسم عبر عملية `native`: ```json { "engine": ">=1.3.0", "requires": { "handlers": ["formulaCopy"] }, "contributes": { "domOps": [{ "op": "native", "handler": "formulaCopy", "params": {} }] } } ``` يختار ملف manifest البدائية ويضبط إعداداتها فقط، ولا يقدّم أي منطق. كما تتحقق البدائية نفسها من `params` قبل أن تعمل. تنطبق قاعدتان على أي إضافة تستخدم بدائية: - يجب أن يعلن كل موقع تطابقه الإضافة المفاتيح الدلالية التي تقرؤها البدائية (فمثلًا تقرأ `mermaid` المفتاحين `assistantTurn` و`codeBlock`). أما `requires.handlers` فاختياري، لأن عملية `native` تحدد البدائية بذاتها. - يجب أن يكون `engine` بالقيمة نفسها أو أعلى من إصدار المحرك الذي شحن البدائية لأول مرة. شُحنت `formulaCopy` في المحرك 1.3.0، لذلك تعلن الإضافة التي تستخدمها `">=1.3.0"`. أما النطاق الأدنى من ذلك فيُفشل `bun run catalog:build`، لأن إصدار Voyager الأقدم سيُبلغ عن handler مفقود بدلًا من أن يطلب من المستخدم التحديث. البدائيات تنمو فقط: أي معامل جديد يكون اختياريًا دائمًا، وأي تغيير كاسر يُشحن باسم جديد. يمكن للإضافة التي يتغيّر سلوكها أن تحمل كذلك سطر `changelog` واحدًا تعرضه النافذة المنبثقة بجوار رقم الإصدار. وتُترجم هذه العبارة ضمن `i18n..changelog` بجانب `name` و`description`. ## متى لا تكون الإضافة التصريحية العادية مناسبة إذا كانت الوظيفة تحتاج إلى تنفيذ JavaScript، أو اعتراض طلبات الشبكة، أو قراءة بيانات Voyager الداخلية أو كتابتها، أو الاعتماد على منطق تشغيل معقّد، فهي لا تناسب الإضافة التصريحية العادية. افتح Issue أولًا واشرح الحاجة. فإذا احتاجت فعلًا إلى قدرة مدمجة، فقد ننظر في تنفيذها داخل مستودع Voyager كإضافة builtin/native، مثل Formula Copy. ## قبل فتح PR شغّل `bun run plugin:check ` والصق مخرجاته في الـ PR. يعرض الأمر كل المشكلات دفعة واحدة، من ملف manifest وCSS إلى احتواء الموقع والبدائيات والمفاتيح الدلالية والترجمات للغات العشر وملف README. وتشغّل CI الفحص نفسه على كل إضافة مرفقة. - الإضافة معطّلة افتراضيًا، والمستخدم هو من يفعّلها. - تحقّقت من عدم وجود إضافة شبه مطابقة؛ وإن وُجدت، فحسّن الإضافة الموجودة أولًا. - اختبرت الموقع المستهدف في السمتين الفاتحة والداكنة. - لا تغطي `matches` مواقع غير ذات صلة. - لا توجد موارد بعيدة. - يحتوي مجلد الإضافة على `plugin.json` وملفات CSS اللازمة وملف README قصير. - إذا كانت إضافة رسمية، فمجلدها يقع ضمن `catalog/sites//plugins//`، وتبقى `matches` الخاصة بها داخل `matches` الموقع، ويُدرجها `catalog/marketplace.json` (باستثناء إضافات Gemini وAI Studio التي تُشحن مع Voyager). - يصف الـ PR صفحات الاختبار واللقطات أو التسجيلات، ومناطق الصفحة المتأثرة. احرص على أن تكون الإضافة بسيطة ومركّزة وقابلة للتراجع. فالإضافة التي تحل مشكلة واحدة واضحة أسهل بكثير في الدمج والصيانة.