Saga Reader 在 0.9.9 版本中迎来了一系列激动人心的更新,显著增强了其功能性、灵活性和用户体验。本次更新的核心亮点包括对更多外部大语言模型(LLM)的支持、引入了经典的 RSS 订阅源、实现了守护进程模式以及增加了用户期待已久的主题切换功能。本文将作为一篇技术博客,深入剖析这些核心功能的实现原理、关键技术点和主要代码实现,帮助开发者和感兴趣的用户更好地理解 Saga Reader 的内部工作机制。
项目介绍:什么是Saga Reader(麒睿智库)
Saga Reader(麒睿智库)是一款基于AI技术的轻量级跨平台阅读器,核心功能涵盖RSS订阅、内容智能抓取、AI内容处理(如翻译、摘要)及本地存储。项目采用Rust(后端)+Svelte(前端)+Tauri(跨平台框架)的技术组合,目标是在老旧设备上实现"低于10MB内存占用"的极致性能,同时提供流畅的用户交互体验。关于Saga Reader的渊源,见《开源我的一款自用AI阅读器,引流Web前端、Rust、Tauri、AI应用开发》。
运行截图
🧑💻码农🧑💻开源不易,各位好人路过请给个小星星💗Star💗。
关键词:端智能,边缘大模型;Tauri 2.0;桌面端安装包 < 5MB,内存占用 < 20MB。
1. 扩充外部模型:拥抱 OpenAI 兼容生态
为了打破特定 LLM 供应商的限制,Saga Reader 0.9.9 版本引入了对所有兼容 OpenAI API 格式的云端大模型的支持。这意味着用户现在可以灵活接入并使用包括但不限于 Groq、Moonshot AI (Kimi)、Yi a以及其他任何提供标准 OpenAI 接口的 LLM 服务。
实现原理与技术点
此功能的核心在于抽象和泛化。我们没有为每一种新的 LLM 服务都编写一套独立的客户端代码,而是创建了一个通用的服务层,专门处理与 OpenAI 兼容 API 的交互。
-
通用服务层
OpenAILikeCompletionService
我们在
crates/llm/src/providers/llm_openaibase_like.rs
文件中定义了OpenAILikeCompletionService
。这个结构体封装了发送请求、处理认证和解析响应的通用逻辑。- 动态配置 :它通过
OpenAILLMProvider
结构(定义于crates/types/src/lib.rs
)接收配置,该结构包含了api_base_url
、api_key
和model_name
等关键信息。 - 标准化请求 :它使用统一的
RequestParameters
结构体来构建请求体,确保与 OpenAI API 的格式完全一致。 - 通用客户端 :内部使用
reqwest
客户端发送 HTTP POST 请求,并通过Authorization
头传入 API Key。
rust:/crates/llm/src/providers/llm_openaibase_like.rs// ... pub struct OpenAILikeCompletionService { pub provider: OpenAILLMProvider, } impl OpenAILikeCompletionService { pub async fn completion(&self, messages: Vec<Message>) -> Result<String, LLMError> { // ... let client = reqwest::Client::new(); let res = client .post(&self.provider.api_base_url) .bearer_auth(&self.provider.api_key) .json(¶ms) .send() .await?; // ... } }
- 动态配置 :它通过
-
重构现有服务
原有的
GLMCompletionService
(智谱 AI)和MistralQinoAgentService
也被重构,改为在内部直接调用OpenAILikeCompletionService
。这极大地简化了代码,并统一了所有云端 LLM 的处理逻辑。rust:/crates/llm/src/providers/llm_glm.rs// ... impl GLMCompletionService { pub async fn completion(&self, messages: Vec<Message>) -> Result<String, LLMError> { let open_ai_like_service = OpenAILikeCompletionService { provider: OpenAILLMProvider { // ... 配置 GLM 的特定参数 }, }; open_ai_like_service.completion(messages).await } }
-
前端配置界面
在设置页面 (
app/src/routes/settings/sections/ai.svelte
),我们为用户提供了清晰的 UI 来配置 OpenAI 兼容服务的 URL、API Key 和模型名称。这些配置会通过 Tauri 的invoke
调用传递给 Rust 后端进行保存和使用。svelte:/app/src/routes/settings/sections/ai.svelte<!-- Svelte code for OpenAI-like provider settings --> <Input label="API URL" bind:value={$llmFormOpenAILikeBaseURI} error={$llmFormOpenAILikeBaseURIErr} /> <Input label="API Key" type="password" bind:value={$llmFormOpenAILikeKey} error={$llmFormOpenAILikeKeyErr} /> <Input label="Model Name" bind:value={$llmFormOpenAILikeModelName} error={$llmFormOpenAILikeModelNameErr} />
2. RSS 订阅源支持:回归经典的内容获取方式
除了基于搜索引擎的智能抓取,0.9.9 版本重新引入了对传统 RSS 订阅源的支持,为用户提供了更稳定、更直接的内容订阅渠道。
实现原理与技术点
该功能的实现依赖于一个统一的内容抓取接口和针对不同源类型的具体实现。
-
统一抓取接口
IFetcher
我们在
crates/scrap/src/types.rs
中定义了一个IFetcher
trait。这个 trait 抽象了所有内容抓取行为,只包含一个核心的fetch
方法,它接收一个源地址(URL 或关键词),返回一个文章列表。rust:/crates/scrap/src/types.rs#[async_trait] pub trait IFetcher { async fn fetch(&self, source: &str) -> Result<Vec<Article>>; }
-
RSSFetcher
的实现在
crates/scrap/src/rss/mod.rs
中,我们创建了RSSFetcher
结构体并为它实现了IFetcher
trait。它使用rss
crate 来解析 RSS feed。- 获取内容 :通过
reqwest
异步获取 RSS URL 的内容。 - 解析 Feed :使用
rss::Channel::read_from
将获取到的 XML 文本解析为结构化的Channel
对象。 - 格式化文章 :遍历
Channel
中的item
,将其转换为我们应用内部统一的Article
结构。
rust:/crates/scrap/src/rss/mod.rs// ... use rss::Channel; #[async_trait] impl IFetcher for RSSFetcher { async fn fetch(&self, url: &str) -> Result<Vec<Article>> { let content = reqwest::get(url).await?.bytes().await?; let channel = Channel::read_from(&content[..])?; let articles = channel.into_items().into_iter().map(|item| { Article { title: item.title().unwrap_or_default().to_string(), url: item.link().unwrap_or_default().to_string(), // ... } }).collect(); Ok(articles) } }
- 获取内容 :通过
-
动态选择抓取器
在核心的
update_feed_contents
函数 (crates/feed_api_rs/src/features/impl_default.rs
) 中,系统会根据订阅源的fetcher_id
(rss
或scrap
)来动态决定使用RSSFetcher
还是原有的ScrapProviderEnums
(搜索引擎抓取)。这种策略模式的设计使得未来扩展更多类型的内容源变得非常容易。rust:/crates/feed_api_rs/src/features/impl_default.rs// ... pub async fn update_feed_contents(&self, ftd: Feed) -> Result<Vec<Article>> { let articles = match ftd.fetcher_id.as_str() { "scrap" => self.scrap_provider.fetch(&ftd.url).await?, "rss" => RSSFetcher::default().fetch(&ftd.url).await?, _ => vec![], }; // ... }
3. 守护进程模式:实现后台静默更新
为了让用户无需时刻打开应用也能及时获取最新资讯,0.9.9 版本引入了守护进程(Daemon)模式。即使在主窗口关闭后,应用依然能在后台静默运行,并定时执行内容更新任务。
实现原理与技术点
此功能主要利用了 Tauri 框架对系统托盘和后台运行的支持。
-
防止应用完全退出
在
tauri.conf.json
中,我们配置了macOSPrivateApi
的close_instead_of_quit
选项为true
。这使得在 macOS 上,当用户点击窗口的关闭按钮时,应用不会完全退出,而是仅仅关闭窗口,主进程继续在后台运行。 -
处理应用重开事件
当应用在后台运行时,如果用户再次点击 Dock 中的图标,我们需要重新显示主窗口。这通过在
crates/tauri-plugin-feed-api/src/lib.rs
中监听RunEvent::Reopen
事件来实现。当该事件触发时,我们会找到主窗口并调用show()
方法。rust:/crates/tauri-plugin-feed-api/src/lib.rs// ... .on_event(|app_handle, event| { if let RunEvent::Reopen { .. } = event { if let Some(window) = app_handle.get_window("main") { let _ = window.show(); let _ = window.set_focus(); } } }) // ...
-
后台定时任务
虽然本次 diff 未直接展示定时任务的创建,但守护进程模式为后台定时任务(如定时刷新所有订阅源)的实现奠定了基础。这类任务通常通过在 Rust 后端启动一个独立的线程或使用像
tokio::time::interval
这样的异步定时器来实现,它会周期性地调用update_feed_contents
函数。
4. 主题切换:个性化的阅读体验
为了提升长时间阅读的舒适度和满足用户的个性化偏好,新版本增加了亮色(Light)和暗色(Dark)主题的切换功能。
实现原理与技术点
该功能的实现是前端技术与 Tauri API 结合的典范。
-
TailwindCSS 暗色模式
我们在
app/tailwind.config.js
中将暗色模式的策略设置为class
。这意味着当<html>
元素包含dark
类名时,所有 Tailwind 的暗色变体(如dark:bg-gray-800
,dark:text-white
)都会被激活。javascript:/app/tailwind.config.jsexport default { // ... darkMode: 'class', // ... };
-
Svelte 状态管理与 Tauri API
在设置页面 (
app/src/routes/settings/+page.svelte
) 中,我们使用 Svelte 的 store 来管理当前的主题状态。switchTheme
函数是核心逻辑所在:- 它首先切换本地的
isDarkModeEnabled
状态。 - 然后,它根据新的状态向
<html>
元素动态添加或移除dark
类。 - 最后,它调用 Tauri 的
appWindow.setTheme
API,将应用窗口本身的主题(如标题栏)也进行同步切换,并持久化用户的选择。
svelte:/app/src/routes/settings/+page.svelte<script lang="ts"> import { appWindow } from '@tauri-apps/api/window'; // ... let isDarkModeEnabled = false; async function switchTheme() { isDarkModeEnabled = !isDarkModeEnabled; const theme = isDarkModeEnabled ? 'dark' : 'light'; if (isDarkModeEnabled) { document.documentElement.classList.add('dark'); } else { document.documentElement.classList.remove('dark'); } await appWindow.setTheme(theme); await setTheme(theme); } onMount(async () => { const theme = await getTheme(); isDarkModeEnabled = theme === 'dark'; }); </script>
- 它首先切换本地的
总结
Saga Reader 0.9.9 版本的更新是全面且深入的。通过拥抱 OpenAI 兼容生态、回归经典的 RSS、实现后台守护进程以及提供个性化的主题切换,Saga Reader 不仅在功能上更加强大和灵活,也在用户体验上迈出了坚实的一步。这些功能的实现充分展示了 Rust 的高性能、Tauri 框架的跨平台能力以及 Svelte 在构建响应式前端界面方面的优势。我们期待这些新功能能为用户带来更高效、更愉悦的阅读和信息获取体验。
📝 Saga Reader系列技术文章
- 开源我的一款自用AI阅读器,引流Web前端、Rust、Tauri、AI应用开发
- 【实战】深入浅出 Rust 并发:RwLock 与 Mutex 在 Tauri 项目中的实践
- 【实战】Rust与前端协同开发:基于Tauri的跨平台AI阅读器实践
- 揭秘 Saga Reader 智能核心:灵活的多 LLM Provider 集成实践 (Ollama, GLM, Mistral 等)
- Svelte 5 在跨平台 AI 阅读助手中的实践:轻量化前端架构的极致性能优化
- Svelte 5状态管理实战:基于Tauri框架的AI阅读器Saga Reader开发实践
- Svelte 5 状态管理全解析:从响应式核心到项目实战
- 【实战】基于 Tauri 和 Rust 实现基于无头浏览器的高可用网页抓取
-Saga Reader 0.9.9 版本亮点:深入解析核心新功能实现