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 的交互。

  1. 通用服务层 OpenAILikeCompletionService

    我们在 crates/llm/src/providers/llm_openaibase_like.rs 文件中定义了 OpenAILikeCompletionService。这个结构体封装了发送请求、处理认证和解析响应的通用逻辑。

    • 动态配置:它通过 OpenAILLMProvider 结构(定义于 crates/types/src/lib.rs)接收配置,该结构包含了 api_base_urlapi_keymodel_name 等关键信息。
    • 标准化请求:它使用统一的 RequestParameters 结构体来构建请求体,确保与 OpenAI API 的格式完全一致。
    • 通用客户端:内部使用 reqwest 客户端发送 HTTP POST 请求,并通过 Authorization 头传入 API Key。
    // ...
    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(&params)
    .send()
    .await?;
    // ...
    }
    }
  2. 重构现有服务

    原有的 GLMCompletionService(智谱 AI)和 MistralQinoAgentService 也被重构,改为在内部直接调用 OpenAILikeCompletionService。这极大地简化了代码,并统一了所有云端 LLM 的处理逻辑。

    // ...
    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
    }
    }
  3. 前端配置界面

    在设置页面 (app/src/routes/settings/sections/ai.svelte),我们为用户提供了清晰的 UI 来配置 OpenAI 兼容服务的 URL、API Key 和模型名称。这些配置会通过 Tauri 的 invoke 调用传递给 Rust 后端进行保存和使用。

    <!-- 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 订阅源的支持,为用户提供了更稳定、更直接的内容订阅渠道。

实现原理与技术点

该功能的实现依赖于一个统一的内容抓取接口和针对不同源类型的具体实现。

  1. 统一抓取接口 IFetcher

    我们在 crates/scrap/src/types.rs 中定义了一个 IFetcher trait。这个 trait 抽象了所有内容抓取行为,只包含一个核心的 fetch 方法,它接收一个源地址(URL 或关键词),返回一个文章列表。

    #[async_trait]
    pub trait IFetcher {
    async fn fetch(&self, source: &str) -> Result<Vec<Article>>;
    }
  2. 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 结构。
    // ...
    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)
    }
    }
  3. 动态选择抓取器

    在核心的 update_feed_contents 函数 (crates/feed_api_rs/src/features/impl_default.rs) 中,系统会根据订阅源的 fetcher_idrssscrap)来动态决定使用 RSSFetcher 还是原有的 ScrapProviderEnums(搜索引擎抓取)。这种策略模式的设计使得未来扩展更多类型的内容源变得非常容易。

    // ...
    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 框架对系统托盘和后台运行的支持。

  1. 防止应用完全退出

    tauri.conf.json 中,我们配置了 macOSPrivateApiclose_instead_of_quit 选项为 true。这使得在 macOS 上,当用户点击窗口的关闭按钮时,应用不会完全退出,而是仅仅关闭窗口,主进程继续在后台运行。

  2. 处理应用重开事件

    当应用在后台运行时,如果用户再次点击 Dock 中的图标,我们需要重新显示主窗口。这通过在 crates/tauri-plugin-feed-api/src/lib.rs 中监听 RunEvent::Reopen 事件来实现。当该事件触发时,我们会找到主窗口并调用 show() 方法。

    // ...
    .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();
    }
    }
    })
    // ...
  3. 后台定时任务

    虽然本次 diff 未直接展示定时任务的创建,但守护进程模式为后台定时任务(如定时刷新所有订阅源)的实现奠定了基础。这类任务通常通过在 Rust 后端启动一个独立的线程或使用像 tokio::time::interval 这样的异步定时器来实现,它会周期性地调用 update_feed_contents 函数。


4. 主题切换:个性化的阅读体验

为了提升长时间阅读的舒适度和满足用户的个性化偏好,新版本增加了亮色(Light)和暗色(Dark)主题的切换功能。

实现原理与技术点

该功能的实现是前端技术与 Tauri API 结合的典范。

  1. TailwindCSS 暗色模式

    我们在 app/tailwind.config.js 中将暗色模式的策略设置为 class。这意味着当 <html> 元素包含 dark 类名时,所有 Tailwind 的暗色变体(如 dark:bg-gray-800, dark:text-white)都会被激活。

    export default {
    // ...
    darkMode: 'class',
    // ...
    };
  2. Svelte 状态管理与 Tauri API

    在设置页面 (app/src/routes/settings/+page.svelte) 中,我们使用 Svelte 的 store 来管理当前的主题状态。switchTheme 函数是核心逻辑所在:

    • 它首先切换本地的 isDarkModeEnabled 状态。
    • 然后,它根据新的状态向 <html> 元素动态添加或移除 dark 类。
    • 最后,它调用 Tauri 的 appWindow.setTheme API,将应用窗口本身的主题(如标题栏)也进行同步切换,并持久化用户的选择。
    <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系列技术文章

Saga Reader 0.9.9 版本亮点:深入解析核心新功能实现的更多相关文章

  1. [译] OpenStack Ocata 版本中的 53 个新功能盘点

    原文链接:https://www.mirantis.com/blog/53-new-things-to-look-for-in-openstack-ocata/ 原文作者:Nick Chase, Ra ...

  2. 【开源】OSharp3.0框架解说系列:新版本说明及新功能规划预览

    OSharp是什么? OSharp是个快速开发框架,但不是一个大而全的包罗万象的框架,严格的说,OSharp中什么都没有实现.与其他大而全的框架最大的不同点,就是OSharp只做抽象封装,不做实现.依 ...

  3. [译] OpenStack Pike 版本中的 53 个新功能盘点

      原文:https://www.mirantis.com/blog/53-things-to-look-for-in-openstack-pike/ 作者:Mirantis Nick Chase 发 ...

  4. IIS6.0,Apache低版本,PHP CGI 解析漏洞

    IIS6.0解析漏洞 在IIS6.0下存在这样的文件"名字.asp;名字.jpg" 代表了jpg文件可以以asp脚本类型的文件执行. 根据这个解析漏洞我们可以上传这种名字类型的图片 ...

  5. FineUIPro/Mvc/Core v5.4.0即将发布(Core基础版,新功能列表)!

    FineUIPro/Mvc/Core v5.4.0 即将于 2019-03-04 发布,目前官网示例已更新,先睹为快:http://pro.fineui.com/http://mvc.fineui.c ...

  6. OpenStack Q版本新功能以及各核心组件功能对比

    OpenStack Q版本已经发布了一段时间了.今天, 小编来总结一下OpenStack Q版本核心组件的各项主要新功能, 再来汇总一下最近2年来OpenStack N.O.P.Q各版本核心组件的主要 ...

  7. Apache Hudi 0.5.1版本重磅发布

    历经大约3个月时间,Apache Hudi 社区终于发布了0.5.1版本,这是Apache Hudi发布的第二个Apache版本,该版本中一些关键点如下 版本升级 将Spark版本从2.1.0升级到2 ...

  8. 【Phylab2.0】Alpha版本项目展示

    团队成员 冯炜韬(PM)http://www.cnblogs.com/toka 岳桐宇(后端)http://www.cnblogs.com/mycraftmw 杨子琛(测试&LaTeX)htt ...

  9. 微信小程序0.11.122100版本新功能解析

    微信小程序0.11.122100版本新功能解析   新版本就不再吐槽了,整的自己跟个愤青似的.人老了,喷不动了,把机会留给年轻人吧.下午随着新版本开放,微信居然破天荒的开放了开发者论坛.我很是担心官方 ...

  10. 完美解决AutoCAD2012,AutoCAD2013本身电脑里有NET4.0或以上版本却装不上的问题

    适用情况:电脑里本身有NET4.0或4.5版本,并且正确安装.或本身你就装有AutoCAD2013或AutoCAD2012要装AutoCAD2012或AutoCAD2013却装不上的情况 如图1所示. ...

随机推荐

  1. TensorFlow 基础 (01)

    以前都自嘲什么码农, 搬砖啥的, 倒不如 "工具人" 这个词更加贴切. 我现在就是一个完完全全的工具人. 上班真的是没有太大乐趣, 如果不下班后培养自己的兴趣爱好, 或者技术精进的 ...

  2. 鸿蒙NEXT(四):后台任务的智能调度策略 — 延迟任务管理解析

    @charset "UTF-8"; .markdown-body { line-height: 1.75; font-weight: 400; font-size: 15px; o ...

  3. Flutter图片组件的定制开发与配置实践

    @charset "UTF-8"; .markdown-body { line-height: 1.75; font-weight: 400; font-size: 15px; o ...

  4. 实现远程磁盘:像访问自己的电脑硬盘一样访问对方的电脑硬盘 (附Demo源码)

    在现实场景中,远程桌面的功能大家已经用得很多了,而在某些场景下,我们需要使用类似的远程磁盘功能,这样能非常方便地操作对方电脑磁盘的目录.以及传送文件.那么,这样的远程磁盘功能要怎么实现了? 这次我们将 ...

  5. RISC-V指令精讲(一):算术指令--加法指令、比较指令

    本节来看下RV32I(32位整数指令集)的算数指令,先学习下加减指令(add.sub),接着了解下数值比较指令(slt),这些指令都有两个版本:一个是立即数版本,一个是寄存器版本 RISCV-V指令格 ...

  6. centos8安装部署RADIUS+MySQLPGSQL高可用架构实现

    以下是针对中大型网络的 RADIUS+MySQL/PGSQL高可用方案 的完整实现,包含数据库集成.主备集群部署和Keepalived配置: 一.MySQL/PGSQL数据库集成(以MySQL为例) ...

  7. 我的Vue之旅(1)

    2020-10-17 今天主要学习了Vue中以下几个指令的使用 v-bind v-if v-on v-for v-model 其中v-bind与v-model都是属于数据绑定,v-bind通常来说是绑 ...

  8. odoo里面的动作

    来源:Odoo中的五种action都是继承自ir.actions.actions模型实现的子类,共有五种,下面会一个一个给出具体例子 1.链接Action(ir.actions.act_url):ta ...

  9. Web前端入门第 61 问:JavaScript 各种对象定义与对象取值方法

    曾经有人说 JS 语言中万物皆对象,虽然这种说法不一定完全准确,但也有一定的道理.原因是 JS 的语法看起来所有的数据类型都像是一个对象,包括原始类型. const a = 1.234; consol ...

  10. kubernetes集群之GC处理

    一.简单说明 GC(Garbage Collector)即垃圾收集清理,kubernetes集群中,kubelet的GC功能将会清理未使用的image和container.其中kubelet对cont ...