主题
系统架构
本文档描述 Lampio 桌面端的当前系统架构。 修改时间以
git log -1 -- docs/architecture.md为准——手写的"最后更新"必然漂移 (本行原写着 2026-03-29,而文件实际一直在改)。
目录
1. 技术选型
| 层 | 选择 | 备选 | 选择理由 |
|---|---|---|---|
| 桌面框架 | Tauri 2.10.3 | Electron | 包体 3-15MB vs 120MB,内存 50-150MB vs 500MB,未来支持 iOS/Android |
| 前端 | React 19 + TypeScript | Vue, Svelte | 生态成熟,TypeScript 严格模式 |
| 构建 | Vite 8 | Webpack | 开发体验,HMR 速度 |
| 样式 | Tailwind CSS 4 | CSS Modules | @theme 自定义主题,实用优先 |
| 状态 | Zustand | Redux, Jotai | 轻量,无 boilerplate,hook-based |
| 本地数据库 | SQLite (双通道) | IndexedDB | 结构化查询,Rust 直接访问 |
| 云端同步 | Supabase | Firebase, 自建 | PostgreSQL + Auth + RLS,免运维 |
| SRS 算法 | SM-2 | FSRS | 与 RVH 移动端统一,2-button 简化 |
2. 系统架构图
┌─────────────────────────────────────────────────────┐
│ Tauri Window │
│ │
│ ┌──────────────────┐ ┌──────────────────────────┐ │
│ │ Main Webview │ │ Content Webview (×N) │ │
│ │ (React App) │ │ content-0, content-1.. │ │
│ │ │ │ │ │
│ │ ┌────────────┐ │ │ ┌────────────────────┐ │ │
│ │ │ Zustand │ │ │ │ content-script.js │ │ │
│ │ │ Stores (7) │ │ │ │ (include_str!) │ │ │
│ │ └────────────┘ │ │ └────────────────────┘ │ │
│ │ │ │ │ │ │ │
│ │ invoke() │ │ __TAURI_INTERNALS__ │ │
│ │ │ │ │ .invoke() │ │
│ └────────┼─────────┘ └────────┼─────────────────┘ │
│ └──────────┬──────────┘ │
│ ▼ │
│ ┌──────────────────────────────────────────────┐ │
│ │ Rust Backend │ │
│ │ ┌─────────┐ ┌─────────┐ ┌───────────────┐ │ │
│ │ │Commands │ │ DbConn │ │ HTTP Client │ │ │
│ │ │ (19个) │ │ (Mutex) │ │ (reqwest) │ │ │
│ │ └────┬────┘ └────┬────┘ └───────┬───────┘ │ │
│ │ │ │ │ │ │
│ └───────┼───────────┼──────────────┼───────────┘ │
└──────────┼───────────┼──────────────┼───────────────┘
│ │ │
┌────▼────┐ ┌────▼────┐ ┌─────▼──────┐
│ rusqlite│ │ tauri- │ │ Supabase │
│ 0.31 │ │plugin-sql│ │ (REST) │
└────┬────┘ └────┬────┘ └─────┬──────┘
└─────┬─────┘ │
┌────▼────┐ ┌─────▼──────┐
│ SQLite │ │ PostgreSQL │
│ (本地) │ sync │ (云端) │
└─────────┘ ◄────► └────────────┘3. Multi-Webview 架构
窗口创建
tauri.conf.json中windows: [](空数组)- 窗口在
setup()中手动创建 - Main webview:
auto_resize = true,label"main" - Content webviews:动态创建,label
"content-{id}"
多标签实现
Tab 0 (active) → content-0 webview → 可见位置
Tab 1 → content-1 webview → offscreen (-10000px)
Tab 2 → content-2 webview → offscreen (-10000px)- 切换标签 = 移动 webview 位置(不销毁)
- 创建标签 =
Window::add_child()新建 webview - 关闭标签 = 销毁 webview
Content-Script 注入
rust
// webview.rs
let script = include_str!("../content-script.js");
webview_builder = webview_builder.initialization_script(script);每个 content webview 创建时自动注入 content-script.js。
4. Rust 后端模块
主要 command 模块,按职责分组(全量清单见 CLAUDE.md §2「Rust Commands 模块」, 当前 23 个 —— 本表只收有独立职责说明价值的,不逐一追平):
| 分组 | 模块 | 职责 |
|---|---|---|
| 核心浏览 | webview | Content webview CRUD + 导航 |
favorite_sites | 站点收藏 | |
history | 资源访问历史 | |
| 词汇学习 | dictionary | 查词(vocabulary 表 + Supabase 缓冲池回填,按词性返回结构化释义) |
vocabulary | 词汇保存/查询/删除(soft-delete 感知) | |
srs | SM-2 复习(2-button Hard/Easy) | |
| 阅读增强 | reading | 阅读难度分析 + Reader 模式 |
translate | AI 翻译(DeepL/DeepSeek/OpenAI) | |
analysis | AI 句子分析 | |
notes | 阅读笔记管理 | |
| 内容源 | rss | RSS 订阅 + 文章缓存 |
text_source | 粘贴文本来源(第三类阅读来源):落库/回填/笔记本查询 + 剪贴板预检。编辑态归 React(components/TextDraftEditor.tsx),本模块只提供命令 | |
| 云端 | auth | Supabase Auth(signup/signin/signout/refresh) |
sync | 同步引擎(push/pull + 墓碑传播)。协议全景见 sync-protocol.md;表数不在这里记(会漂),见 database-tables-overview.md §8 | |
snapshot | 快照文件「在哪、缺了怎么办」:library 路径解析(按用户隔离)+ 云端按需取回 + 存量目录迁移 | |
supabase | Supabase REST 工具 | |
| 基础设施 | settings | Key-value 设置存储 |
data | CSV 导入/导出 | |
init | 应用初始化 | |
anki | Anki 导出 |
5. 前端状态管理
8 个 Zustand stores:
| Store | 职责 | 持久化 |
|---|---|---|
useTabsStore | 多标签状态(tabs[], activeTabId, CRUD) | 否 |
useVocabStore | 词汇列表、过滤、分页 | 否 |
useAuthStore | Auth session + 自动同步生命周期(60s polling) | token 持久化到 settings 表 |
useSettingsStore | 翻译 API 配置、主题等 | settings 表 |
useNavigationStore | 当前 URL、导航状态 | 否 |
useTranslationStore | 翻译面板状态 | 否 |
useThemeStore | 明暗主题切换 | localStorage |
useReviewStore | 复习卡片 + Focus Review 状态机(isInFocusSession / sessionFilter / preFocusSnapshot / startSession / endSession) | 否 |
顶层导航(Workspace 模块 / ActivityBar)
useWorkspaceStore.activeModule 驱动左 rail(ActivityBar)在 7 个认知模式间硬切换: Discover / Read / Review / Library / Stats(上区)+ Settings / Account(下区)。启动默认落 discover(不持久化)。
- Discover = 全宽渲染
HomePage(推荐/站点/feeds/续读),产品进水口。Home 不再是 tab——App-shell 重构(2026-08-01,app-shell-redesign-plan.md)后从 Read 的 tab 系统移出、成独立模块(useTabsStore初始tabs:[REVIEW_TAB]、activeTabId:'')。点文章/站点 → 开 tab 并切到 Read。 - Read = 浏览 tab 条(
TitleBar)+AddressBarchrome;无浏览 tab 时显ReadEmptyState(引导去 Discover)。 - 顶栏语汇(方向 A,2026-08-01):标题栏是一条独立的栏(
bg-surface-chrome,不与下方 toolbar/内容黏连), 跨模块统一。Read 顶栏放浏览器 tab,但激活态 = 浮起的白色圆角 pill(bg-surface-content+shadow,TitleBar),未激活 = 纯文字——不再是 Chrome 融合式。其余模块顶栏 =ModuleTitleBar(红绿灯 + 左对齐 安静模块标题,无 tab 药丸、无图标、不印 Lampio)——rail 已是顶层导航,标题栏不复述它。子区切换下沉到内容 顶部做段控(SegmentedGroup+TabButton):Library[词汇│笔记](LibraryPanel顶)、Review[复习│重温](ReviewPanel卡侧栏顶);单一表面模块(Discover/Stats/Settings/Account)只有标题、无段控。已弃用的FocusReviewHeader/LibrarySectionHeader/StatsHeader/SettingsHeader/AccountHeader退役。 内容区顶角为直角(去rounded-t-2xl,内容与 chrome 框平齐)。
面板互斥(3-3):左辅助面板(Sites/History/RSS/TOC)与右辅助面板(Discovery/Annotations/Pronunciation/ Tools)互斥——usePanelStore 开一侧自动收另一侧,任一时刻最多一个辅助面板占阅读宽度。
Focus Review 模式(Review 模块内的派生 chrome)
Review 是一个 workspace 模块(非 Read 内的临时态)。进入即 enterReviewModule(快照 pre-review tab/panel + isInFocusSession=true),离开由 ActivityBar.switchModule 调 endSession 恢复。isInFocusSession 是单一真相 (I1:仅 enterReviewModule/endSession 翻转)。派生 chrome:
TitleBar→ 替换为ModuleTitleBar(「回顾」标题)AddressBar→ focus 段(AddressBarReviewSegment):Zone1(侧栏宽)= 复习/重温 段控(操作切换归 toolbar); Zone2 = 源页文章标题 + 多源翻页 + reader,隐藏跨内容导航- 卡侧栏(
ReviewPanel)顶部 =ReviewSessionHeader(‹ 返回+ 组/笔记标题 +N/M进度)——会话上下文 紧贴其描述的词卡(2026-08-01 与 toolbar 的模式段控交换位置) - 复习卡侧栏在最左(
inFocusReview,可拖宽);右 panel 保留(查词语境),左 panel + Find 关闭 ReviewSessionComplete= 完成庆祝页signOut→ 必须先 endSession 再 resetAllTabs(I3)- 残留:复习期点 Settings/Account 走
switchModule → endSession会丢会话;Modal 逃逸延 backlog(Item C)
6. Content-Script 架构
src-tauri/src/content-script.js 是注入到每个 content webview 的纯 JS 文件,包含 8 个功能模块:
| 模块 | 功能 | 触发方式 |
|---|---|---|
| 双击查词 | caretRangeAtPoint + Selection API 提取单词 | dblclick 事件 |
| 弹窗 | 释义、音标、CEFR、例句、TTS 按钮 | 查词后 DOM 注入 |
| 词汇保存 | 自动保存到 learning_entries + 上下文句子 | 查词时自动 |
| CEFR 高亮 | A1-C2 颜色编码,按节点 TreeWalker 遍历 | 页面加载后 |
| Reader 模式 | 提取正文,隐藏广告/导航 | 用户触发 |
| 选区翻译 | 选中文本经 Edge Function translate-batch 翻译 | 选区工具栏 |
| TTS | Azure Neural 服务端合成(失败自动落系统语音) | 弹窗按钮 |
| 微复习 | Hard/Easy 两键复习已学单词 | 弹窗内 |
关键约束
- IPC:必须使用
window.__TAURI_INTERNALS__.invoke()(不能用@tauri-apps/api) - MutationObserver:DOM 操作前必须
disconnect(),操作后重新observe() - SQL:禁止
LOWER(),用COLLATE NOCASE
7. IPC 通信模式
Main Webview → Rust
typescript
// src/lib/commands.ts
import { invoke } from '@tauri-apps/api/core';
const result = await invoke('command_name', { arg1, arg2 });Content Webview → Rust
javascript
// content-script.js(不能使用 @tauri-apps/api)
const result = await window.__TAURI_INTERNALS__.invoke('command_name', { arg1, arg2 });Rust → Frontend(事件)
rust
app.emit_to("main", "event_name", payload)?;8. Auth & Sync 数据流
认证流程
用户登录 → auth.rs sign_in()
→ Supabase REST /auth/v1/token
→ 返回 access_token + refresh_token
→ 持久化到 settings 表
→ useAuthStore 启动 60s auto-sync polling同步协议
🧭 全景已收敛到 sync-protocol.md(2026-09-01),本节不再维护第二份。 那里讲:增量游标为什么用服务端时间戳 · 脏判定的三列 · 墓碑的三分支与两个守卫 · 冲突怎么裁 · 四个静默失效家族与五条不变量的关系 · 快照通道 · 协议版本与用户切换。
本节此前那份流程图长期停在已废弃的协议上(写着 WHERE updated_at > last_sync_at, 而 RB 红线 #5 早已改成 server_updated_at),同步表映射表也还列着一张不存在的 word_contexts —— 这正是「同一事实在两处并行维护」的现成例证。
| 想知道 | 去处 |
|---|---|
| 同步怎么运作、为什么这么定 | sync-protocol.md |
| 条文(做错会出事的那句话) | ../CLAUDE.md §4 · ../src-tauri/CLAUDE.md §2 |
| 逐表 yes/no 与列级映射 | database-tables-overview.md §8 · database-schema.md §9 |
9. 关键技术决策
| 决策 | 选择 | 原因 |
|---|---|---|
| Tauri 而非 Electron | Tauri 2.0 | 包体小 10-40x,内存低 3-5x,原生性能 |
| SM-2 而非 FSRS | SM-2 | 与 RVH 移动端统一算法,2-button 序列推导更简单 |
| rusqlite 锁定 0.31 | 0.31 | 与 tauri-plugin-sql 共享 libsqlite3-sys 0.28,升级会冲突 |
| TEXT UUID 主键 | TEXT | 跨设备同步需要全局唯一 ID,INTEGER 自增在多端会冲突 |
| Soft-delete | deleted_at 列 | 跨端同步需要传播删除操作,hard delete 会丢失信息 |
| Content-script 注入 | include_str!() | 编译时嵌入,无需运行时加载文件 |
| Supabase 而非自建 | Supabase | PostgreSQL + Auth + RLS 开箱即用,免运维 |
| COLLATE NOCASE | 替代 LOWER() | SQLite 的 LOWER() 不支持 Unicode |