TypeScript rss & rss-reader repository by @zhanglun. AI agent rules & system prompts with 1.8k stars.
# AGENTS.md
面向 AI 编码代理的仓库速查笔记。内容与可执行配置冲突时,以配置为准。
## 项目形态
- Lettura 是 Tauri v2 桌面订阅阅读器:React/Vite 前端 + Rust 后端(Cargo
workspace 成员 `apps/desktop/src-tauri`)。
- 两套 workspace 共享仓库树:pnpm 管理 JS 包(`apps/desktop`、`apps/docs`、
未来的 `packages/*`),根 Cargo.toml 管理全部 Rust(成员
`apps/desktop/src-tauri` 与 `packages/*`)。pnpm 会静默忽略 Rust 目录;
Cargo.lock 只保留根目录一份。
- 订阅抓取器是 `packages/fetcher-*` 下的独立 crate(`fetcher-core` =
`Fetcher` trait + `FetchedArticle`;`fetcher-rss` = 解析/发现层;
`fetcher-mail` = IMAP(async-imap 0.10 + mail-parser 0.11,两者均已锁
feature);`fetcher-bilibili` = wbi 签名 Web API)。抓取器必须无状态,
禁止依赖 diesel/tauri/actix;注册表、探测分发(`claims` 顺序、rss 兜底)
与 `FetchedArticle → NewArticle` 转换器在 `src-tauri/src/fetchers/mod.rs`;
按 `feeds.provider` 的同步分发在 `feed/channel.rs::sync_articles`。
新增源类型 = 新 crate + 注册表加一行。每源配置存在
`feeds.provider/account_uuid/source_config`;凭据在 `source_accounts`
表(`sources/account_service.rs`,命令
`list/save/delete/test_source_account`)。邮件同步会把 `last_uid`
水位写回 `source_config`。
- 前端数据访问只走 localhost HTTP(`src/helpers/http.ts` 的
`apiGet/apiPost`,对准内嵌 Actix 服务 `http://127.0.0.1:{port}/api`,
实现在 `src-tauri/src/server/handlers/`)。dataAgent 已移除(2026-09-30,
提交 5a8278b4 回滚了此前的 IPC-only 收拢)。Tauri `invoke` 仅保留非数据
命令(窗口/端口、OPML、来源账户、订阅添加/预览)。新数据操作 =
新增 Actix handler。
- 前端数据流是两条车道(保持现状):文章列表查询走 `src/hooks/useArticle.ts`
(模块级缓存 + 在途去重,列表状态的唯一真相源);命令类 POST 可直接
`apiPost`,然后经 `store.getSubscribes()` 和/或
`busChannel.emit("getChannels")` 刷新(AppLayout 监听一次)。改变文章
全集的命令(退订/删除源等)成功后必须调
`invalidateArticleCache()`(useArticle.ts 导出,含在途世代守卫),否则
返回列表页会命中陈旧缓存。不要引入第三层缓存,不要把列表状态塞回
Zustand slice。
- React Compiler 1.0 已接入 vite.config.ts 与 vitest.config.ts
(babel-plugin-react-compiler):src 下所有组件/钩子都被编译(零
bail-out)并自动记忆化。禁止再写 useCallback/useMemo/React.memo——
记忆化由编译器负责,手写hook 曾经直接阻断编译("Existing memoization
could not be preserved")。塑造了现有代码形态的编译器约束(务必继续
遵守):渲染期禁止写 ref(在 useEffect 里同步);组件/钩子内禁止
try/finally 语句(用 promise 的 .catch()/.finally());组件体内禁止
import() 表达式(提升到模块级);渲染期禁止读可变模块级状态(缓存、
单例)——编译器会把这类读当作纯计算缓存住陈旧值。跨渲染数据一律走
state 传递,缓存在 effect 里同步(键切换不允许画出旧帧时用
useLayoutEffect)。
- 前端入口:`src/index.tsx` 定义路由并在 Tauri 内等待 `get_server_port`;
`src/App.tsx` 是应用外壳与 Tauri 事件监听;路由名在 `src/config.ts`。
- Rust 入口:`src-tauri/src/main.rs` 调用 `lettura_lib::run()`(定义在
`src-tauri/src/lib.rs`):加载配置、打开 SQLite、跑内嵌 Diesel 迁移、
启动 Actix 服务、注册 Tauri 命令、托盘/菜单处理器与调度器。
- 状态是单一 Zustand store(`useAppStore`,`src/stores/index.ts`),由
feed、article、user config、podcast 四个 slice 组合而成。
## 常用命令
- 安装:`pnpm install`(`.npmrc` 设了 `auto-install-peers=true`)。
- 仅前端开发:`pnpm dev`(Vite 固定端口 9527)。
- 桌面完整开发:`pnpm tauri dev`(经 `src-tauri/tauri.conf.json` 运行
`pnpm dev`)。
- 前端构建/类型检查:`pnpm build`(`tsc && vite build`);Vite 输出到
`build/`,不是 `dist/`。
- 桌面构建:`pnpm tauri build`。
- 前端测试:`pnpm test`;单文件:`pnpm test path/to/file.test.ts`。
- Rust 测试:在仓库根跑 `cargo test`(workspace 成员是 `src-tauri` 与
`packages/*`);`pnpm cargo:check|cargo:test|cargo:fmt` 是便捷封装。
- Lint/格式化用 Biome(`apps/desktop/biome.json`),不是 ESLint/Prettier:
`npx biome check src/` 与 `npx biome format src/`(在 `apps/desktop/`
下执行)。
## 运行时与存储要点
- Vite 固定 `server.strictPort = true`,端口 9527。Actix API 服务先用配置
端口,被占用则回落 8000-9000。
- Tauri 之外,`src/index.tsx` 把 `localStorage.port` 默认为 `3456`;Tauri
内渲染前调用 `invoke("get_server_port")`。
- 生产 SQLite 库是 `~/.lettura/lettura.db`。设置 `LETTURA_ENV` 后,Rust 经
`dotenv` 从环境变量读 `DATABASE_URL`。
- 用户配置是 TOML(正常运行为 `~/.lettura/lettura.toml`,dev 模式为本地
文件),不在 SQLite schema 内。
- 播客数据是独立的浏览器端 Dexie/IndexedDB(`src/helpers/podcastDB.ts`),
读写统一收口在 `stores/createPodcastSlice.ts` 的 action 与渲染端
useLiveQuery。
- 关闭主窗口是隐藏到系统托盘,不是退出(release;dev 直接退出)。窗口控制
跨平台:macOS 走 `tauri.macos.conf.json`(平台配置合并,windows 数组
整体替换,须带全量字段)在创建期即 `decorations: true` + Overlay——
decorations:false 的 borderless 窗口没有标准按钮,且运行时补
set_decorations 会与 Overlay 异步竞态(实测内容不延伸);红绿灯由 lib.rs
`layout_traffic_lights` 摆位(wry 的 trafficLightPosition 纵向无效),
顶栏经 `body.platform-mac` 让位左侧 100px。Windows/Linux 用自绘
`components/layout/WindowControls`(顶栏右端三钮)。拖拽区仍由
`data-tauri-drag-region` 承担(双击 = 最大化切换)。
## 测试要点
- Vitest 使用 `vitest.config.ts`:开启 globals,jsdom 环境,setup 文件为
`src/__tests__/setup.ts`(测试管线同样挂了 React Compiler 编译器)。
- setup 文件 mock 了 `localStorage`、`@tauri-apps/api/core` 的 `invoke`、
`@tauri-apps/api/event`、`@tauri-apps/api/webviewWindow`、
`@tauri-apps/plugin-shell`、`@tauri-apps/plugin-fs`、
`@tauri-apps/plugin-dialog` 和全局 `fetch`;测试在没有真实 Tauri 后端时
也能通过,判断测试结果时要记住这一点。
- 前端测试主要在 `src/stores/__tests__/`、`src/helpers/__tests__/`、
`src/hooks/__tests__/`;Rust 测试如 `src-tauri/src/cmd.rs`、
`src-tauri/src/core/scheduler.rs`。
## 样式与 i18n
- Tailwind v3 + Radix UI 主题令牌配置在 `tailwind.config.js`;暗色模式基于
class/data-attribute,应用切换 `body.dark-theme`。
- shadcn 风格组件在 `src/components/ui/`;组合类名优先用
`src/helpers/cn.tsx` 的 `cn`。
- 0.2.0 的 UI 由 `src/styles/fusion.css`(全局 `fusion-*` 类)承载,经
`src/index.css` → `styles/index.css` 分层引入;Astryx 主题来自 npm 包
`@astryxdesign/theme-*`。无 PWA/service worker(vite-plugin-pwa 已移除,
忽略提及它的旧笔记)。
- i18n 在 `src/i18n.ts` 初始化;语言包为 `src/locales/en.json` 与
`src/locales/zh.json`。
## Rust 后端要点
- Diesel schema 输出在 `src-tauri/src/schema.rs`;迁移在
`src-tauri/migrations/`,由 `embed_migrations!` 内嵌。
- 调度器(`core/scheduler.rs`)按到期驱动:每 60s tick 一次,同步
`last_sync_date + (sync_interval 或 update_interval)` 已到期的源,然后每
个源 emit `sync://completed` `{uuid, title, inserted, error}`(在
`src/App.tsx` 监听)。源行的 `last_sync_date` 以本地时间存
"YYYY-MM-DD HH:MM:SS" 字符串。
- Rust 模块按职责拆分:`core/`(配置/菜单/调度/托盘)、`feed/`(文章/
频道/文件夹/OPML)、`server/`(Actix 路由)。托盘与菜单在 `setup()` 里
用 `TrayIconBuilder`/`MenuBuilder` 构建。
- Tauri v2 是插件架构:shell、fs、dialog、http、process、updater、log、
single-instance 都是独立插件(Rust crate 与 npm 包成对)。权限声明在
`src-tauri/capabilities/default.json`(取代 v1 的 allowlist)。
- `LETTURA_ENV` 开启调试日志并改变配置/数据库行为;不要假设 dev 与生产
路径一致。
## CI 与发布
- `.github/workflows/release.yml` 在 push 到 `release` 时运行:创建 draft
release,再用 Tauri 构建 macOS(arm/x64/universal)、Ubuntu、Windows 产物。
自动更新链路:conf 开了 `bundle.createUpdaterArtifacts`(安装包 + 签名的
updater 产物),多平台 latest.json 由 tauri-action 合并上传——publish 前
人工核对 draft 里 latest.json 应含全部平台条目;客户端入口在设置页
「行为与数据 → 检查更新」(`UpdaterRow`,手动检查,无启动静默检查)。
- `.github/workflows/deploy-doc.yml` 在 `master` 上运行,把 `docs/` 的
Astro 文档站部署到 GitHub Pages。
- `package.json`、`src-tauri/tauri.conf.json`、`src-tauri/Cargo.toml` 三处
版本号保持同步(bump 后本地 `pnpm cargo:check` 刷 Cargo.lock)。
- **版本号规则(2026-10-11 用户定)**:除非用户明确指定版本号,发布一律在
0.2.x 线内 patch 递增(0.2.1 → 0.2.2 → …);升 minor(如 0.3.0)必须
用户点名。
- 发布顺序:三处版本号 bump 提交 → 推 `0.2.0` 分支与 `master`(ff)→
`git push origin 0.2.0:release` 触发构建 → 人工核对 draft 的 latest.json
全平台条目 → publish。