跳到內容
Oleaflyv0.4.7

Oleafly 如何在前端工作區、Rust 服務和固定版本的 sidecar 之間劃分桌面應用程式。

Oleafly 的前端是一個 pnpm 工作區:應用程式殼層位於儲存庫根目錄(src/),可重複使用的前端程式碼則以十二個職責明確的套件形式放在 packages/*。Tauri 後端位於 src-tauri/,可重複使用的 AI 供應商與代理程式碼則位於 crates/oleafly-agent/。前端/Rust/sidecar 的整體圖像請參閱 README 的架構圖 (在新分頁開啟),日常指令請參閱開發。

本文件涵蓋:每個套件是什麼、讓它們保持純粹的相依反轉規則、把功能接到殼層的貢獻註冊表、模組解析如何運作,以及如何新增或抽出套件。

過去每個功能都直接伸進應用程式的 Zustand store、Tauri IPC 用戶端和 shadcn UI 套件。這讓功能與殼層無法分開:沒有任何東西能單獨測試、重複使用或推理。工作區把這件事反過來。套件持有功能邏輯,並為它們需要的一切宣告稱為埠(port)的窄介面。應用程式在它的 store 和 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貢獻註冊表:側欄分頁、面板/全能列命令、AI 工具集(詳見下方)無(貢獻自帶行為)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/editorLaTeX、Typst 和 Markdown 的 CodeMirror 核心:CodeMirrorEditor、編輯器檢視控制器(insertAtCursor、gotoLine……)、語言與自動完成、主題、摺疊、固定標題、檢查器、latex-mask、公式預覽(LaTeX 用 KaTeX,Typst 用 Typst)、Typst 編輯輔助與 LaTeX 轉 Typst 的數學式轉換、LaTeX 和 Typst 的就地視覺化模式(@oleafly/editor/visual)、搜尋面板、拼字/文法檢查器EditorHost(文件模型與設定,hook 形式)、SpellHost(拼字檢查器/Harper/字典)、setBibKeysProvider、extraExtensions/extraKeymap@codemirror/*、@replit/codemirror-vim、@replit/codemirror-emacs、katex
@oleafly/preview虛擬化的 pdf.js 檢視器(PdfViewer)、SyncTeX 頁面控制器(gotoRect、pageClickToBp)、pdf.js worker,以及 WebView polyfill(@oleafly/preview/polyfills,在 main.tsx 中最先匯入)onOpenLink prop(系統瀏覽器連結)、setPdfLoggerpdfjs-dist
@oleafly/diagramTikZ、Typst(fletcher)和 Mermaid 的視覺化圖表編輯器:React Flow 畫布、形狀檢查器、繪圖/程式碼分頁,以及編譯、預覽、插入的流程。src/languages/ 中的每種語言都有寫入器、讀取器,以及它的插入與檔案形式,而畫布與程式碼雙向保持同步DiagramHost(LaTeX 編譯、Typst 與 Mermaid 繪製、檔案 IO、編輯器插入、AI 修正)、DiagramKit context(Button/Tooltip/Select/toast/主題)@xyflow/react、@codemirror/*、@oleafly/latex、@oleafly/editor
@oleafly/ai-tools專案、研究、記憶和圖片工具的定義,其變更操作會配合核准機制AiToolsHost(檔案、編譯、符號索引、圖片管線、編輯器)@oleafly/latex
@oleafly/templates新專案範本庫(兩步驟精靈、分類、預覽、附進度的一次性資產下載)TemplatesHost(預覽、資產下載、記錄)、TemplatesKit(Button/Tooltip),顏色選項以 props 傳入除了 UI 工具之外沒有其他

相依方向,嚴格執行:

應用程式殼層與套件如何互相依賴src/ 中的應用程式殼層包含 Zustand store、UI 套件、Tauri 用戶端,以及埠的轉接器。轉接器會實作 @oleafly 功能套件所宣告的埠,例如 EditorHost,並由應用程式注入。功能套件可以依賴純粹的末端套件,例如 latex、ai-core 和 registry。Tauri 用戶端透過 Tauri IPC 與 Rust 後端溝通。套件絕不會匯入應用程式、它的 store 或 Tauri API。應用程式殼層,src/@oleafly/* 套件StoreZustand轉接器實作埠UI 套件shadcnTauri 用戶端src/lib/tauri.ts功能套件宣告埠,例如 EditorHost末端套件latex, ai-core, registryRust 後端src-tauri, crates/oleafly-agent注入Tauri IPC套件絕不會匯入應用程式、它的 store 或 Tauri API。應用程式殼層與套件如何互相依賴src/ 中的應用程式殼層包含 Zustand store、UI 套件、Tauri 用戶端,以及埠的轉接器。轉接器會實作 @oleafly 功能套件所宣告的埠,例如 EditorHost,並由應用程式注入。功能套件可以依賴純粹的末端套件,例如 latex、ai-core 和 registry。Tauri 用戶端透過 Tauri IPC 與 Rust 後端溝通。套件絕不會匯入應用程式、它的 store 或 Tauri API。應用程式殼層,src/@oleafly/* 套件StoreUI 套件Tauri 用戶端轉接器Rust 後端src-tauri, crates功能套件宣告埠末端套件latex, ai-core, registryTauri IPC注入套件絕不會匯入應用程式、它的 store 或 Tauri API。
應用程式相依於套件,並為套件宣告的埠注入轉接器。套件絕不會匯入應用程式。

套件絕不能從 src/(@/…)、Zustand store、@tauri-apps/* 或應用程式的 UI 元件匯入。純粹性檢查是抽出作業衛生的一部分:

終端機視窗
grep -rn 'from "@/\|@tauri\|zustand' packages/*/src # must return nothing

套件宣告介面。應用程式只實作一次,通常是以模組層級的轉接器物件,並包住它的 store。有四種反覆出現的形式:

主機埠:每個套件一個 interface XHost,涵蓋它需要的服務。應用程式用命令式的 store 讀取來建立轉接器:

// 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 中,所以前端套件只會收到它們的介面所需要的狀態和結果。

UI 套件組:結構型別元件的 React context(或 prop):Button: ComponentType<{ variant?: …; onClick?: () => void }>。應用程式原封不動地傳入它的 shadcn 元件,因為較小的結構型別是相容的。套件組也帶有 toast 和 useThemeMode() hook。

hook 形式的埠:用於必須訂閱應用程式狀態的套件。編輯器的 EditorHost 有 hook 成員(useActivePath()、useSettings()),以及給事件監聽器用的命令式對應成員(getActivePath())。規則:主機物件必須是模組層級的(身分穩定),而它的 use* 成員必須無條件呼叫 hooks。

模組單例:用於橫切的黏合程式碼,setPdfLogger(fn)、setSpellHost(host)、setBibKeysProvider(fn) 有無作用的預設值,並由應用程式的 shim 在匯入時的副作用中安裝。

每次抽出都在舊的 @/… 路徑留下一個很薄的檔案,重新匯出(或包裝)該套件,所以使用端和現有的 vi.mock(...) 路徑從未變動。例子有:src/components/editor/cm/controller.ts(重新匯出)、src/components/pdf/PdfViewer.tsx(注入 shell-plugin 連結開啟器的包裝)、src/lib/ai-tools.ts(轉接器加上原本的工廠簽章)。新的程式碼可以直接匯入 @oleafly/*。相容層是為了相容性而存在,不是偏好的路徑。

@oleafly/registry 是功能在殼層不認識它們的情況下接入應用程式殼層的方式。共有三種貢獻類型:

  • 側欄分頁(registerRailTab):圖示、區段(explore/review/assist,在非空的區段之間繪製分隔線)、選用的 when(ctx) 可見性、選用的 useBadge() hook(例如 Git 變更數量),以及側邊欄的 panel 元件。
  • 命令(registerCommand):透過 surfaces 提供給指令面板(Cmd+K,依 group 分組)和/或全能列(Cmd+P)。標籤和圖示可以是 context 的函式(例如「切換至淺色主題」)。
  • AI 工具集(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"),
});

這是內部 API,不是外掛 SDK:形式可以隨時變更,也沒有任何東西是動態載入的。在出現真正的使用者之前,暫不處理:把匯出選單格式、設定結構描述和專案類型行為做成貢獻。

因為套件是以原始碼的形式使用,同一個別名要在三個地方宣告,新增套件時三處都必須保持同步:

  1. tsconfig.json → compilerOptions.paths。像 @oleafly/preflight/pdf-extract 這樣的子路徑需要自己明確的項目。
  2. vite.config.ts → resolve.alias。字串別名以前綴比對,所以子路徑不需要另外的項目就能解析。
  3. vitest.config.ts → 同樣的 resolve.alias,加上 test.include 必須涵蓋 packages/**/*.test.ts。否則被搬移的測試會悄悄地從執行中消失。

另外:根目錄的 package.json 透過 "workspace:*" 相依於每個套件,而 src/styles/globals.css 有 @source "../../packages",讓 Tailwind v4 掃描套件原始碼中的 class 名稱。

僅限 Vite 的匯入(?worker&url worker、CSS 匯入)從套件原始碼使用時可以照常運作,因為別名指向原始碼。請讓任何會牽入 pdf.js 的東西遠離 node 環境測試所匯入的 barrel:這就是 pdf-extract 和 preview 套件的檢視器只能在有 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 store、Tauri IPC 用戶端(src/lib/tauri.ts)、shadcn UI 套件、版面配置/殼層元件、埠的轉接器和相容層、src/contributions/,以及天生與 store 耦合的功能黏合程式碼(預檢面板 UI、行內 AI 編輯外掛,以及以 extraExtensions 注入編輯器的程式碼或懸停智慧功能)。Tauri 命令和作業系統整合留在 src-tauri/。供應商用戶端、串流和代理程式編排屬於 crates/oleafly-agent/,這樣密鑰和供應商請求就留在 webview 之外。

圖片預覽

無法載入 AI 回答

請再試一次,或搜尋文件。