Bỏ qua để đến nội dung
Oleaflyv0.4.7

Cách Oleafly phân chia ứng dụng desktop giữa workspace frontend, các dịch vụ Rust và các sidecar được ghim phiên bản.

Frontend của Oleafly là một pnpm workspace: app shell nằm ở thư mục gốc của repo (src/), còn mã frontend dùng lại được nằm trong packages/* dưới dạng mười hai package tập trung. Backend Tauri nằm trong src-tauri/, còn mã provider AI và agent dùng lại được nằm trong crates/oleafly-agent/. Xem sơ đồ kiến trúc trong README (mở trong thẻ mới) để có bức tranh frontend/Rust/sidecar, và xem Phát triển để biết các lệnh hằng ngày.

Tài liệu này bao gồm: mỗi package là gì, các quy tắc đảo ngược phụ thuộc giữ cho chúng thuần khiết, contribution registry gắn các tính năng vào shell, cách phân giải module vận hành, và cách thêm hay tách một package.

Trước đây, mọi tính năng đều truy cập trực tiếp vào các Zustand store của app, Tauri IPC client và bộ shadcn UI kit. Điều đó khiến các tính năng không thể tách rời shell: không gì có thể được kiểm thử, dùng lại hay suy luận một cách độc lập. Workspace đảo ngược điều đó. Các package giữ logic của tính năng và khai báo các interface hẹp gọi là port cho mọi thứ chúng cần. App hiện thực các port đó trên nền store và Tauri client của mình rồi tiêm chúng vào. App phụ thuộc vào package, không bao giờ ngược lại.

Các package được tiêu thụ dưới dạng mã nguồn TypeScript (main của chúng trỏ tới src/index.ts). Không có bước build, không xuất bản, không lệch phiên bản: Vite, Vitest và tsc đều phân giải @oleafly/* thẳng tới mã nguồn của package qua alias (xem Phân giải module).

PackageLà gìPort được tiêmDependency đáng chú ý
@oleafly/latexLogic thuần cho LaTeX và hình vẽ: mô hình diagram, bộ tuần tự hóa TikZ (modelToTikz, khứ hồi embedded-model), bộ dựng tài liệu standalone (buildStandaloneDoc), các hàm hỗ trợ về tên hình và kích thước bytekhông có (thuần khiết)không có
@oleafly/ai-coreSiêu dữ liệu hiển thị của provider và việc chọn model phía frontend, được giữ ngang hàng với catalog Rustkhông có (thuần khiết)không có
@oleafly/latex-intelligenceCác kiểu đã xác thực và truy cập dữ liệu cho kho ngữ liệu LaTeX được dùng bởi trí thông minh của trình soạn thảokhông có (thuần khiết)không có
@oleafly/registryContribution registry: thẻ rail, lệnh palette/omnibar, bộ công cụ AI (chi tiết bên dưới)không có (mỗi contribution tự mang hành vi của nó)react (types)
@oleafly/preflightBộ máy preflight: phân loại tài liệu, quy tắc cho nguồn/PDF/tham chiếu, phân tích ATS, chuẩn bị xuất bản tiếp cận được, chấm điểm. pdf-extract phát hành dưới dạng subpath @oleafly/preflight/pdf-extract để pdf.js không lọt vào đồ thị kiểm thử nodekhông có (thuần khiết)pdfjs-dist (chỉ subpath)
@oleafly/pdf-to-latexPipeline phục hồi PDF sang LaTeX chạy cục bộ: gom dòng, nhận diện tiêu đề và cột, suy luận kiểu dáng, ánh xạ ký hiệu, trích xuất hình, thoát ký tự và lắp ráp tài liệu. Adapter pdf.js phát hành dưới dạng subpath @oleafly/pdf-to-latex/pdf-adapter vì cùng lý do đồ thị kiểm thử nodekhông có (thuần khiết)pdfjs-dist (chỉ subpath)
@oleafly/wysiwygLõi trình soạn thảo trực quan có ràng buộc: schema dùng chung, phân tích và tuần tự hóa LaTeX với Markdown, cùng các node inline/block thô giúp phần nguồn chưa được hỗ trợ vẫn hiển thị thay vì bị loại bỏkhông có (thuần khiết)không có
@oleafly/editorLõi CodeMirror cho LaTeX, Typst và Markdown: CodeMirrorEditor, bộ điều khiển editor-view (insertAtCursor, gotoLine, …), ngôn ngữ + hoàn thiện, theme, gấp đoạn, tiêu đề dính, linter, latex-mask, xem trước công thức (KaTeX cho LaTeX, Typst cho Typst), công cụ hỗ trợ soạn thảo Typst và chuyển đổi công thức LaTeX sang Typst, chế độ Visual tại chỗ cho LaTeX và Typst (@oleafly/editor/visual), panel tìm kiếm, linter chính tả và ngữ phápEditorHost (mô hình tài liệu + cài đặt, dạng hook), SpellHost (kiểm tra chính tả/Harper/từ điển), setBibKeysProvider, extraExtensions/extraKeymap@codemirror/*, @replit/codemirror-vim, @replit/codemirror-emacs, katex
@oleafly/previewTrình xem pdf.js ảo hóa (PdfViewer), bộ điều khiển trang SyncTeX (gotoRect, pageClickToBp), pdf.js worker, và các polyfill WebView (@oleafly/preview/polyfills, được import đầu tiên trong main.tsx)prop onOpenLink (liên kết mở bằng trình duyệt hệ thống), setPdfLoggerpdfjs-dist
@oleafly/diagramTrình soạn diagram trực quan cho TikZ, Typst (fletcher) và Mermaid: canvas React Flow, trình kiểm tra hình khối, các thẻ Draw/Code, luồng biên dịch, xem trước và chèn. Mỗi ngôn ngữ trong src/languages/ có một writer, một reader cùng dạng chèn và dạng tệp của nó, và canvas với mã luôn bước đồng bộ theo cả hai chiềuDiagramHost (biên dịch LaTeX, kết xuất Typst và Mermaid, IO tệp, chèn vào trình soạn thảo, sửa bằng AI), ngữ cảnh DiagramKit (Button/Tooltip/Select/toast/theme)@xyflow/react, @codemirror/*, @oleafly/latex, @oleafly/editor
@oleafly/ai-toolsĐịnh nghĩa công cụ cho dự án, nghiên cứu, bộ nhớ và hình vẽ, kèm các mutation có kiểm soát phê duyệtAiToolsHost (tệp, biên dịch, chỉ mục ký hiệu, pipeline hình vẽ, trình soạn thảo)@oleafly/latex
@oleafly/templatesThư viện mẫu dự án mới (thuật sĩ hai bước, phân loại, xem trước, tài nguyên tải về một lần kèm tiến độ)TemplatesHost (xem trước, tải tài nguyên, ghi nhật ký), TemplatesKit (Button/Tooltip), tùy chọn màu truyền qua propskhông có ngoài các tiện ích UI

Hướng phụ thuộc, được thi hành nghiêm ngặt:

Vỏ ứng dụng và các gói phụ thuộc vào nhau thế nàoVỏ ứng dụng trong src/ chứa các store Zustand, bộ UI, ứng dụng khách Tauri và các adapter cổng. Các adapter triển khai những cổng mà các gói tính năng @oleafly khai báo, như EditorHost, và ứng dụng tiêm chúng vào. Gói tính năng có thể phụ thuộc vào các gói lá thuần như latex, ai-core và registry. Ứng dụng khách Tauri trò chuyện với backend Rust qua Tauri IPC. Các gói không bao giờ import ứng dụng, store của nó hay API Tauri.Vỏ ứng dụng, src/Các gói @oleafly/*StoreZustandAdapterTriển khai cổngBộ UIshadcnỨng dụng khách Taurisrc/lib/tauri.tsGói tính năngKhai báo cổng như EditorHostGói lálatex, ai-core, registryBackend Rustsrc-tauri, crates/oleafly-agentTiêm vàoTauri IPCCác gói không bao giờ import ứng dụng, store của nó hay API Tauri.Vỏ ứng dụng và các gói phụ thuộc vào nhau thế nàoVỏ ứng dụng trong src/ chứa các store Zustand, bộ UI, ứng dụng khách Tauri và các adapter cổng. Các adapter triển khai những cổng mà các gói tính năng @oleafly khai báo, như EditorHost, và ứng dụng tiêm chúng vào. Gói tính năng có thể phụ thuộc vào các gói lá thuần như latex, ai-core và registry. Ứng dụng khách Tauri trò chuyện với backend Rust qua Tauri IPC. Các gói không bao giờ import ứng dụng, store của nó hay API Tauri.Vỏ ứng dụng, src/Các gói @oleafly/*StoreBộ UIỨng dụng khách TauriAdapterBackend Rustsrc-tauri, cratesGói tính năngKhai báo cổngGói lálatex, ai-core, registryTauri IPCTiêm vàoCác gói không bao giờ import ứng dụng,store của nó hay API Tauri.
Ứng dụng phụ thuộc vào các gói và tiêm adapter cho những cổng mà chúng khai báo. Các gói không bao giờ import ứng dụng.

Một package tuyệt đối không được import từ src/ (@/…), từ một Zustand store, từ @tauri-apps/*, hay từ các UI component của app. Việc kiểm tra độ thuần khiết là một phần của quy trình vệ sinh khi tách package:

Cửa sổ terminal
grep -rn 'from "@/\|@tauri\|zustand' packages/*/src # must return nothing

Các package khai báo interface. App hiện thực chúng một lần, thường dưới dạng một object adapter cấp module bao lấy các store của mình. Bốn dạng thường gặp:

Host port: mỗi package có một interface XHost bao quát các dịch vụ mà nó cần. App dựng adapter bằng các lệnh đọc store trực tiếp:

// 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);

Giữ các mối quan tâm riêng của app bên trong adapter cũng giữ các dependency khỏi lọt vào package. Thông tin xác thực và các yêu cầu tới provider nằm ở phía Rust, nên các package frontend chỉ nhận đúng trạng thái và kết quả mà interface của chúng yêu cầu.

UI kit: một React context (hoặc prop) chứa các component định kiểu theo cấu trúc: Button: ComponentType<{ variant?: …; onClick?: () => void }>. App truyền các component shadcn của mình qua nguyên dạng vì kiểu cấu trúc nhỏ hơn vẫn tương thích. Bộ kit còn mang toast và hook useThemeMode().

Port dạng hook: dành cho các package bắt buộc phải theo dõi trạng thái của app. EditorHost của trình soạn thảo có các thành viên dạng hook (useActivePath(), useSettings()) cùng cặp song sinh trực tiếp (getActivePath()) cho các trình lắng nghe sự kiện. Quy tắc: object host phải nằm ở cấp module (định danh ổn định) và các thành viên use* của nó phải gọi hook một cách vô điều kiện.

Singleton cấp module: cho phần nối xuyên suốt, setPdfLogger(fn), setSpellHost(host), setBibKeysProvider(fn) có default không làm gì và được shim của app cài đặt như một tác dụng phụ khi import.

Mỗi lần tách đều để lại một tệp mỏng tại đường dẫn @/… cũ, tệp này tái xuất (hoặc bọc) package, để các bên tiêu thụ và các đường dẫn vi.mock(...) sẵn có không hề xáo trộn. Ví dụ: src/components/editor/cm/controller.ts (tái xuất), src/components/pdf/PdfViewer.tsx (wrapper tiêm bộ mở liên kết của shell plugin), src/lib/ai-tools.ts (adapter + các chữ ký factory gốc). Mã mới có thể import @oleafly/* trực tiếp. Shim tồn tại để tương thích, không phải là đường dẫn ưu tiên.

@oleafly/registry là cách các tính năng cắm vào app shell mà shell không cần biết đến chúng. Ba loại contribution:

  • Thẻ rail (registerRailTab): biểu tượng, nhóm (explore / review / assist, vạch phân cách vẽ giữa các nhóm không trống), khả năng hiển thị qua when(ctx) tùy chọn, hook useBadge() tùy chọn (ví dụ số thay đổi của Git), và component panel của sidebar.
  • Lệnh (registerCommand): được phục vụ cho command palette (Cmd+K, nhóm theo group) và/hoặc omnibar (Cmd+P) qua surfaces. Nhãn và icon có thể là hàm theo ngữ cảnh (ví dụ: “Switch to light theme”).
  • Bộ công cụ AI (registerAiToolset): một chế độ chat ("chat", "figure") được ánh xạ tới một factory dựng bộ công cụ. Panel chat tra bộ công cụ của chế độ đang hoạt động thay vì ghi cứng một bộ.

AppContext ({ projectId, projectKind, theme }) được từng bề mặt kết dựng tạo ra từ các subscription của chính nó, nên các điều kiện when/label luôn phản ứng kịp thời.

Việc đăng ký là tĩnh: src/contributions/ đăng ký mọi thứ tích hợp sẵn (thẻ, lệnh, bộ công cụ) và main.tsx gọi registerContributions() một lần trước khi shell được gắn. Rail, Sidebar, CommandPalette, SearchOmnibar và ChatPanel chỉ kết xuất những gì đã đăng ký.

Để thêm thẻ hay lệnh cho một tính năng, hãy đăng ký nó. Không cần sửa shell:

// 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"),
});

Đây là API nội bộ, không phải plugin SDK: các hình dạng có thể thay đổi tự do, và không gì được nạp động. Trì hoãn tới khi có bên tiêu thụ thực sự: định dạng của menu xuất, schema cài đặt, và hành vi theo loại dự án dưới dạng contribution.

Vì các package được tiêu thụ dưới dạng mã nguồn, cùng một alias được khai báo ở ba nơi, và cả ba phải giữ đồng bộ khi thêm một package:

  1. tsconfig.json → compilerOptions.paths. Subpath như @oleafly/preflight/pdf-extract cần mục khai báo riêng tường minh.
  2. vite.config.ts → resolve.alias. Alias dạng chuỗi khớp theo tiền tố, nên subpath phân giải được mà không cần thêm mục nào.
  3. vitest.config.ts → cùng resolve.alias đó, cộng thêm test.include phải bao packages/**/*.test.ts. Nếu không, các bài kiểm thử đã di chuyển sẽ lặng lẽ rơi khỏi lượt chạy.

Ngoài ra: package.json gốc phụ thuộc vào mỗi package qua "workspace:*", và src/styles/globals.css có @source "../../packages" để Tailwind v4 quét mã nguồn các package tìm tên lớp.

Các import chỉ dành cho Vite (worker ?worker&url, import CSS) hoạt động nguyên trạng từ mã nguồn package vì alias trỏ thẳng vào mã nguồn. Hãy giữ mọi thứ kéo theo pdf.js ra khỏi các barrel mà bài kiểm thử môi trường node import: đó là lý do pdf-extract và trình xem của package preview chỉ truy cập được ở nơi có DOM.

  • Bài kiểm thử nằm cạnh mã nguồn (packages/*/src/*.test.ts) và chạy trong cùng pnpm test với bài kiểm thử của app. Hãy để ý số lượng bài kiểm thử khi di chuyển tệp.
  • Dependency được khai báo lỏng ("react": "*"). pnpm khử trùng lặp theo gốc, nơi đặt giới hạn dưới thực của các phiên bản. Đừng khai báo dependency mà package không import.
  • Cổng kiểm soát cho mọi thay đổi package: pnpm exec tsc --noEmit, pnpm build, pnpm test (số lượng không đổi), và lệnh grep độ thuần khiết ở trên.
  • Tách phần mới: di chuyển tệp bằng git mv (lịch sử đi theo), định nghĩa các port, để shim tại đường dẫn cũ, nối dây ba alias, rồi chạy các cổng kiểm soát. Đừng tách một tính năng trong lúc bạn đang chỉnh sửa nó liên tục.
  • Giấy phép: mọi package đều là AGPL-3.0-or-later, giống app.

src/ giữ mọi thứ thực sự thuộc về app frontend: các Zustand store, Tauri IPC client (src/lib/tauri.ts), bộ shadcn UI kit, các component layout/shell, các adapter và shim của port, src/contributions/, và phần keo nối tính năng vốn gắn chặt store (UI panel preflight, plugin chỉnh sửa inline-AI, và trí thông minh mã hoặc hover được tiêm vào trình soạn thảo dưới dạng extraExtensions). Lệnh Tauri và phần tích hợp hệ điều hành nằm trong src-tauri/. Client provider, streaming và điều phối agent thuộc về crates/oleafly-agent/ để thông tin mật và yêu cầu tới provider luôn nằm ngoài webview.

Xem trước hình ảnh

Không tải được câu trả lời AI

Hãy thử lại, hoặc tìm trong tài liệu.