بنية الواجهة الأمامية
كيف يوزّع Oleafly تطبيق سطح المكتب بين مساحة عمل الواجهة الأمامية، وخدمات Rust، والبرامج المرافقة ذات الإصدارات المثبّتة.
الواجهة الأمامية لـ Oleafly هي مساحة عمل pnpm: يعيش هيكل التطبيق في جذر المستودع (src/)، ويعيش رمز الواجهة الأمامية القابل لإعادة الاستخدام في packages/* على هيئة اثنتي عشرة حزمة مركّزة. وتعيش خلفية Tauri في src-tauri/، بينما يعيش رمز مزوّدي الذكاء الاصطناعي والوكيل القابل لإعادة الاستخدام في crates/oleafly-agent/. انظر مخطط البنية في README (يفتح في علامة تبويب جديدة) للاطلاع على صورة الواجهة الأمامية/Rust/البرامج المرافقة، وانظر التطوير لأوامر العمل اليومية.
يغطّي هذا المستند: ما هي كل حزمة، وقواعد عكس التبعيات التي تحافظ على نقائها، وسجل المساهمات الذي يربط الميزات بالهيكل، وكيفية عمل تحليل الوحدات، وكيفية إضافة حزمة أو استخراجها.
لماذا مساحة عمل؟
قسم بعنوان “لماذا مساحة عمل؟”كانت كل ميزة في السابق تصل مباشرة إلى مخازن Zustand في التطبيق، وعميل Tauri IPC، وطقم واجهة shadcn. وقد جعل ذلك الميزات غير قابلة للفصل عن الهيكل: فلم يعد بالإمكان اختبار أي شيء أو إعادة استخدامه أو التفكير فيه على نحو معزول. تقلب مساحة العمل هذا الوضع. تحمل الحزم منطق الميزات وتصرّح بواجهات ضيقة تُسمى المنافذ لكل ما تحتاجه. ينفّذ التطبيق تلك المنافذ فوق مخازنه وعميل Tauri ثم يحقنها. يعتمد التطبيق على الحزم، ولا يحدث العكس أبدًا.
تُستهلك الحزم كمصدر TypeScript (يشير حقل main في كل منها إلى src/index.ts). لا توجد خطوة بناء، ولا نشر، ولا انحراف في الإصدارات: يحلّ كل من Vite وVitest وtsc الرموز @oleafly/* مباشرة إلى مصادر الحزم عبر الأسماء البديلة (انظر تحليل الوحدات).
الحزم
قسم بعنوان “الحزم”| الحزمة | ما هي | المنافذ المحقونة | تبعيات بارزة |
|---|---|---|---|
@oleafly/latex | منطق LaTeX والأشكال النقي: نموذج المخطط، ومسلسل TikZ (modelToTikz، ودورة ذهاب وعودة للنماذج المضمّنة)، وباني المستند المستقل (buildStandaloneDoc)، ومساعدات أسماء الأشكال وأحجامها بالبايتات | لا شيء (نقي) | لا شيء |
@oleafly/ai-core | بيانات عرض المزوّدين واختيار النماذج في الواجهة الأمامية، مع الحفاظ على تطابقها مع فهرس Rust | لا شيء (نقي) | لا شيء |
@oleafly/latex-intelligence | أنواع متحقَّق منها ووصول إلى البيانات لمدونة LaTeX التي تستخدمها ذكاء المحرر | لا شيء (نقي) | لا شيء |
@oleafly/registry | سجل المساهمات: علامات تبويب الشريط، وأوامر لوحة الأوامر/شريط البحث الشامل، وأطقم أدوات الذكاء الاصطناعي (التفاصيل أدناه) | لا شيء (المساهمات تحمل سلوكها معها) | react (الأنواع) |
@oleafly/preflight | محرك الفحص المسبق: تصنيف المستندات، وقواعد المصدر/PDF/المراجع، وتحليل ATS، وتجهيز التصدير الميسّر، والتقييم. يُشحن pdf-extract كمسار فرعي @oleafly/preflight/pdf-extract ليبقى pdf.js خارج مخططات اختبار node | لا شيء (نقي) | pdfjs-dist (المسار الفرعي فقط) |
@oleafly/pdf-to-latex | مسار إعادة بناء PDF إلى LaTeX المحلي: تجميع الأسطر، وكشف العناوين والأعمدة، واستنباط الأنماط، وربط الرموز، واستخراج الأشكال، وتهريب المحارف، وتجميع المستند. يُشحن محوّل pdf.js كمسار فرعي @oleafly/pdf-to-latex/pdf-adapter للسبب نفسه المتعلق بمخطط اختبار node | لا شيء (نقي) | pdfjs-dist (المسار الفرعي فقط) |
@oleafly/wysiwyg | نواة المحرر المرئي المقيّد: المخطط المشترك، وتحليل LaTeX وMarkdown وتسلسلهما، وعقد السطر والكتلة الخام التي تُبقي المصدر غير المدعوم مرئيًا بدلًا من إهماله | لا شيء (نقي) | لا شيء |
@oleafly/editor | نواة CodeMirror لـ LaTeX وTypst وMarkdown: CodeMirrorEditor، ومتحكم عرض المحرر (insertAtCursor، gotoLine، …)، واللغات مع الإكمالات، والمظهر، والطي، والعناوين اللاصقة، وأدوات الفحص، وقناع latex-mask، ومعاينة الرياضيات (KaTeX لـ LaTeX، وTypst لـ Typst)، ومساعدات تحرير Typst وتحويل الرياضيات من LaTeX إلى Typst، ووضع Visual في الموضع لـ LaTeX وTypst (@oleafly/editor/visual)، ولوحة البحث، وأدوات فحص الإملاء والقواعد | EditorHost (نموذج المستند + الإعدادات، بشكل خطافات)، SpellHost (المدقق الإملائي/Harper/القاموس)، setBibKeysProvider، extraExtensions/extraKeymap | @codemirror/*, @replit/codemirror-vim, @replit/codemirror-emacs, katex |
@oleafly/preview | عارض pdf.js الافتراضي (PdfViewer)، ومتحكم صفحات SyncTeX (gotoRect، pageClickToBp)، وعامل pdf.js، وسدّادات WebView (@oleafly/preview/polyfills، تُستورد أولًا في main.tsx) | خاصية onOpenLink (روابط متصفح النظام)، setPdfLogger | pdfjs-dist |
@oleafly/diagram | مؤلّف المخططات المرئي لـ TikZ وTypst (fletcher) وMermaid: لوحة React Flow، ومفتّش الأشكال، وعلامتا تبويب الرسم/الشيفرة، وتدفق التجميع ثم المعاينة ثم الإدراج. لكل لغة في src/languages/ كاتب وقارئ وصيغتا إدراج وملف خاصتان بها، وتبقى اللوحة والشيفرة متزامنتين في الاتجاهين | DiagramHost (تجميع LaTeX، وعرض Typst وMermaid، وإدخال/إخراج الملفات، والإدراج في المحرر، والإصلاح بالذكاء الاصطناعي)، وسياق DiagramKit (Button/Tooltip/Select/toast/theme) | @xyflow/react, @codemirror/*, @oleafly/latex, @oleafly/editor |
@oleafly/ai-tools | تعريفات أدوات المشروع والبحث والذاكرة والأشكال مع تعديلات واعية بالموافقة | AiToolsHost (الملفات، والتجميع، وفهرس الرموز، ومسار الأشكال، والمحرر) | @oleafly/latex |
@oleafly/templates | معرض قوالب المشاريع الجديدة (معالج من خطوتين، وفئات، ومعاينات، وتنزيلات لمرة واحدة للموارد مع إظهار التقدم) | TemplatesHost (المعاينات، وتنزيل الموارد، والتسجيل)، TemplatesKit (Button/Tooltip)، وخيارات الألوان كخصائص | لا شيء عدا أدوات واجهة المستخدم |
اتجاه التبعيات، مفروض بصرامة:
يجب ألا تستورد أي حزمة أبدًا من src/ (@/…)، ولا من مخزن Zustand، ولا من @tauri-apps/*، ولا من مكونات واجهة التطبيق. فحص النقاء جزء من نظافة الاستخراج:
grep -rn 'from "@/\|@tauri\|zustand' packages/*/src # must return nothingنمط المنافذ
قسم بعنوان “نمط المنافذ”تصرّح الحزم بالواجهات. وينفّذها التطبيق مرة واحدة، عادةً على هيئة كائن مهايئ على مستوى الوحدة يحتفظ بمخازن التطبيق في نطاقه. أربعة أشكال متكررة:
منافذ المضيف: واجهة interface XHost واحدة لكل حزمة تغطّي الخدمات التي يحتاجها. يبني التطبيق المهايئ بقراءات أمرية من المخازن:
// src/lib/ai-tools.ts (app adapter, abridged)const HOST: AiToolsHost = { getProjectId: () => useFilesStore.getState().projectId, readFileContent, // Tauri client passthrough recompile: () => useCompileStore.getState().recompile(), insertAtCursor, // editor controller // ...};export const createOleaflyTools = (opts?) => createOleaflyToolsCore(HOST, opts);إبقاء الشواغل الخاصة بالتطبيق وحده داخل المهايئ يُبقي التبعيات خارج الحزم أيضًا. تبقى بيانات اعتماد المزوّدين والطلبات في Rust، فلا تتلقى حزم الواجهة الأمامية إلا الحالة والنتائج التي تتطلبها واجهاتها.
أطقم واجهة المستخدم: سياق React (أو خاصية) من مكونات ذات أنواع بنيوية: Button: ComponentType<{ variant?: …; onClick?: () => void }>. يمرّر التطبيق مكونات shadcn دون تغيير لأن النوع البنيوي الأصغر متوافق. تحمل الأطقم أيضًا toast وخطاف useThemeMode().
منافذ بشكل خطافات: للحزم التي يجب أن تشترك في حالة التطبيق. يملك EditorHost الخاص بالمحرر أعضاء على هيئة خطافات (useActivePath()، useSettings()) إضافة إلى توائم أمرية (getActivePath()) لمستمعي الأحداث. القواعد: يجب أن يكون كائن المضيف على مستوى الوحدة (بهوية مستقرة)، ويجب أن تستدعي أعضاء use* فيه الخطافات دون قيد.
مفردات على مستوى الوحدة: للرابط العابر للمجالات؛ تملك setPdfLogger(fn) وsetSpellHost(host) وsetBibKeysProvider(fn) قيمًا افتراضية لا تفعل شيئًا، ويثبّتها مهايئ التطبيق كأثر جانبي للاستيراد.
الملفات البديلة
قسم بعنوان “الملفات البديلة”تركت كل عملية استخراج ملفًا رقيقًا عند مسار @/… القديم يعيد تصدير الحزمة (أو يغلّفها)، بحيث لم يضطر المستهلكون ومسارات vi.mock(...) القائمة إلى التغيير مطلقًا. أمثلة: src/components/editor/cm/controller.ts (إعادة تصدير)، وsrc/components/pdf/PdfViewer.tsx (غلاف يحقن فاتح الروابط الخاص بملحق الهيكل)، وsrc/lib/ai-tools.ts (مهايئ + تواقيع المصنع الأصلية). يمكن للشيفرة الجديدة استيراد @oleafly/* مباشرة. توجد الملفات البديلة لأغراض التوافق، وليست المسار المفضّل.
سجل المساهمات
قسم بعنوان “سجل المساهمات”@oleafly/registry هي الطريقة التي تتصل بها الميزات بهيكل التطبيق من دون أن يعرفها الهيكل. ثلاثة أنواع من المساهمات:
- علامات تبويب الشريط (
registerRailTab): الأيقونة، والقسم (explore/review/assist، مع فواصل تُرسم بين الأقسام غير الفارغة)، وإظهارwhen(ctx)الاختياري، وخطافuseBadge()الاختياري (مثل عدد تغييرات Git)، ومكوّنpanelللشريط الجانبي. - الأوامر (
registerCommand): تُقدَّم إلى لوحة الأوامر (Cmd+K، مجمّعة حسبgroup) و/أو شريط البحث الشامل (Cmd+P) عبرsurfaces. وقد تكون التسميات والأيقونات دوال للسياق (مثل «التبديل إلى المظهر الفاتح»). - أطقم أدوات الذكاء الاصطناعي (
registerAiToolset): وضع محادثة ("chat"،"figure") يقترن بمصنع طقم أدوات. تبحث لوحة المحادثة عن طقم أدوات الوضع النشط بدلًا من تثبيته في الشيفرة.
يبني كل سطح عرض AppContext ({ projectId, projectKind, theme }) من اشتراكاته الخاصة، فتبقى شروط when/label تفاعلية.
التسجيل ثابت: يسجّل src/contributions/ كل ما هو مدمج (علامات التبويب والأوامر وأطقم الأدوات)، وتستدعي main.tsx الدالة registerContributions() مرة واحدة قبل تحميل الهيكل. ولا تعرض Rail وSidebar وCommandPalette وSearchOmnibar وChatPanel إلا ما هو مسجّل.
لإضافة علامة تبويب أو أمر خاص بميزة ما، سجّله. من دون أي تعديل على الهيكل:
// src/contributions/my-feature.tsx (called from src/contributions/index.ts)registerRailTab({ id: "review", label: "Review", icon: MessageSquare, section: "review", order: 55, when: (ctx) => ctx.projectKind !== "image", panel: ReviewPanel,});registerCommand({ id: "omnibar.review", surfaces: ["omnibar"], order: 45, label: "Open review", keywords: "review comments", run: () => useSettingsStore.getState().setRailTab("review"),});هذه واجهة برمجية داخلية، وليست حزمة تطوير إضافات: قد تتغير الأشكال بحرية، ولا يُحمَّل أي شيء ديناميكيًا. وأُجِّل إلى حين وجود مستهلك حقيقي: تنسيقات قائمة التصدير، ومخططات الإعدادات، وسلوكيات أنواع المشاريع بوصفها مساهمات.
تحليل الوحدات
قسم بعنوان “تحليل الوحدات”لأن الحزم تُستهلك كمصدر، يُصرَّح بالاسم البديل نفسه في ثلاثة أماكن، ويجب أن تبقى الثلاثة متزامنة عند إضافة حزمة:
tsconfig.json→compilerOptions.paths. تحتاج المسارات الفرعية مثل@oleafly/preflight/pdf-extractإلى مدخل صريح خاص بها.vite.config.ts→resolve.alias. الأسماء البديلة النصية تطابق البادئات، فتُحلّ المسارات الفرعية دون مدخل آخر.vitest.config.ts→ اسمresolve.aliasنفسه، إضافةً إلى أنtest.includeيجب أن يغطيpackages/**/*.test.ts. وإلا انسحبت الاختبارات المنقولة من التشغيل بصمت.
أيضًا: يعتمد package.json في الجذر على كل حزمة عبر "workspace:*"، ويحتوي src/styles/globals.css على @source "../../packages" ليفحص Tailwind v4 مصادر الحزم بحثًا عن أسماء الأصناف.
تعمل الاستيرادات الخاصة بـ Vite وحده (العمّال بصيغة ?worker&url، واستيرادات CSS) دون تغيير من مصادر الحزم لأن الأسماء البديلة تشير إلى المصدر. أبقِ أي شيء يجذب pdf.js خارج ملفات barrel التي تستوردها اختبارات بيئة node: لهذا لا يمكن الوصول إلى pdf-extract وعارض حزمة المعاينة إلا حيث توجد DOM.
العمل على الحزم
قسم بعنوان “العمل على الحزم”- الاختبارات تعيش بجوار المصدر (
packages/*/src/*.test.ts) وتعمل ضمن أمرpnpm testنفسه مع اختبارات التطبيق. راقب عدد الاختبارات عند نقل الملفات. - التبعيات يُصرَّح بها بتساهل (
"react": "*"). يزيل pnpm التكرار مقابل الجذر، حيث توجد الحدود الدنيا الحقيقية للإصدارات. لا تصرّح بتبعيات لا تستوردها الحزمة. - بوابات أي تغيير في حزمة:
pnpm exec tsc --noEmit، وpnpm build، وpnpm test(بالعدد نفسه)، وفحص grep للنقاء أعلاه. - استخراج شيء جديد: انقل الملفات عبر
git mv(يتتبعها السجل)، وحدّد المنافذ، واترك ملفات بديلة عند المسارات القديمة، واربط الأسماء البديلة الثلاثة، ثم شغّل البوابات. لا تستخرج ميزة أثناء تطويرك النشط عليها. - الترخيص: كل حزمة
AGPL-3.0-or-later، مثل التطبيق نفسه.
ما يبقى في التطبيق
قسم بعنوان “ما يبقى في التطبيق”يحتفظ src/ بكل ما هو حقًا تطبيق الواجهة الأمامية: مخازن Zustand، وعميل Tauri IPC (src/lib/tauri.ts)، وطقم واجهة shadcn، ومكونات التخطيط/الهيكل، ومهايئات المنافذ والملفات البديلة، وsrc/contributions/، والشيفرة الرابطة للميزات المرتبطة بالمخازن بطبيعتها (واجهة لوحة Preflight، وملحق التحرير بالذكاء الاصطناعي المضمّن، وذكاء الشيفرة أو التحويم المحقون في المحرر بوصفه extraExtensions). تبقى أوامر Tauri وتكامل نظام التشغيل في src-tauri/. أما عملاء المزوّدين والبث وتنسيق الوكيل فمكانها crates/oleafly-agent/ لتبقى الأسرار وطلبات المزوّدين خارج الـ webview.