桌面悬浮窗 Sprite:把系统监控、番茄钟与随身 AI 收进一块常驻小屏

作者:

🇬🇧 English

引言

AI 写代码写到一半,想看一眼 CPU 是不是又飙了——我现在已经不怎么自己动手写了,就是敲敲提示词、跟几个 AI 聊聊需求,经常是好几个 AI IDE 并行执行编码任务,CPU 风扇狂转,不用看仪表我也知道已经顶到 100% 了,真得有个东西常驻在眼前随时扫一眼;可切到活动监视器太打断心流。定了 25 分钟番茄钟,计时器却藏在浏览器某个标签页里;常用的一堆内部网页和工具散落在书签栏、Dock、各个地方;账号密码记在备忘录里;脑子里冒出个关于本机的问题想问 AI,却得再开一个 App……

这些动作单个都不起眼,但一天里反复发生,就是无数次上下文切换。多 Agent 并行确实快,可代价是:机器早就不是一个人的了,总得有块小屏替你盯着。Sprite 就是这块屏——平时半透明地浮在角落,把系统监控、专注计时、常用入口、密钥和随身 AI 都收进一个窗口。

它其实是个多模块的小工具。下面我按自己每天遇到的几种"桌面碎事",讲它怎么落地、源码长什么样。

速览(TL;DR)

  • Sprite 是 Tauri v2 + React 的常驻桌面悬浮窗,把系统监控 / 番茄钟 / 快捷启动 / 密码本 / 内置 LLM / 多 Harness 收进一个窗口
  • 系统监控走 macOS 原生 df -H(磁盘)+ sysinfo(CPU/内存),前端 5 秒轮询,50% / 80% 双阈值颜色预警
  • 内置 LLM 随时就"本机系统状态"提问(读实时数据回答"谁在吃 CPU"),并可接入 resolve-studio / spring-harness 等 Harness 当随身 Agent 控制台
  • 密码本在设置里以「自定义配置」添加,内容本地加密存储、不落明文,可选主密码加锁,本地私有、不出网,整个应用可设锁屏密码
  • Tauri v2 打包约 25MB(macOS 复用系统 WebView 而非 Chromium),内存稳定在 80–120MB
  • 最小化展示:点 把整块悬浮窗收成 360×36 常驻小横条(Esc / 还原),各面板也能单独收起
  • 剪贴板历史保留最近 20 条、敏感内容(密钥/卡号/身份证)自动跳过;全局热键 Cmd+Option+D 显隐;对话可导出 Markdown / JSON

常驻但不抢戏:最小化展示

前面这些能力,如果个个都占一块大窗口、还抢焦点,它本身就成了新的干扰源。Sprite 的底线是"最小化展示"——它先是一个足够小的东西,再决定要不要变大。

窗口本身就是为"不打扰"生的。它浮在角落,不进 Dock 任务流、不抢焦点,工作时在余光里;想看时余光一扫就有。

窗口长什么样,其实 tauri.conf.json 里这几行就定死了:

{
  "app": {
    "macOSPrivateApi": true,
    "windows": [
      {
        "title": "Sprite",
        "transparent": true,
        "alwaysOnTop": true,
        "decorations": false,
        "shadow": false,
        "resizable": true,
        "width": 420,
        "height": 500,
        "minWidth": 340,
        "minHeight": 400
      }
    ]
  }
}

decorations:false(无系统标题栏)、transparent:true(半透明背景)、alwaysOnTop:true(常驻置顶),初始尺寸只有 420×500。注意 macOSPrivateApi:true——透明窗在 macOS 上必须开这个私有 API 开关,否则透明区域会 fallback 成黑色。

更收的时候,点窗口上的 能把整块悬浮窗收成一条 360×36 的常驻横条(useMinimize.tsMIN_W=360MIN_H=36)——CPU/内存、H/S/R 快捷开窗仍在条上,Esc 一键还原,置顶与位置都保留,多显示器下也跟着当前窗口走、不跑错屏。单个面板(如 ResolvePanel)也能各自收起成小条,只看关心的那一块。托盘图标则负责在窗口被收走后随时把它唤回来。

这就回到开头的场景:AI 们写代码写到一半,扫一眼 CPU——它本就该是这么轻、这么近的一个存在,轻到不配有"打开 / 关闭"的仪式感。

常驻之外还有两个让"隐身"更顺手的入口:全局热键 Cmd+Option+D 一键显隐整块悬浮窗(在设置里可改成别的组合,保存即自动重注册,hotkey.rs);需要大屏时点 setSize + setPosition 把窗口铺满当前显示器——这里刻意不用 macOS 原生全屏:原生全屏会独占一个 Space 把其它窗口推开,而且透明窗口叠加原生全屏是已知的黑屏组合(App.tsx 注释已记下这个坑)。

显隐背后的 Rust 命令很直白——先用 unregister_all 清掉旧绑定避免重复注册,再挂 on_shortcut 监听按下事件,按当前可见性决定 hide / show + set_focus

// hotkey.rs
#[tauri::command]
pub fn register_toggle_hotkey(app: AppHandle, hotkey_str: String) -> Result<(), String> {
    let shortcut = parse_hotkey(&hotkey_str)?;
    app.global_shortcut().unregister_all().ok(); // 先清掉旧绑定
    app.global_shortcut().on_shortcut(shortcut, |app, _s, event| {
        if event.state() != ShortcutState::Pressed { return; }
        if let Some(win) = app.get_webview_window("main") {
            if win.is_visible().unwrap_or(false) {
                let _ = win.hide();
            } else {
                let _ = win.show();
                let _ = win.set_focus();
            }
        }
    })?;
    Ok(())
}

系统状态随时看:常驻悬浮窗与系统监控

第一个诉求最直接:系统资源常看,但不想为此切出当前工作。Sprite 把这个做成了悬浮窗常驻桌面——半透明、不抢焦点,一眼就能扫到 CPU、内存、磁盘。

后端 system_stats.rs 负责数据采集。这里有个容易想错的点:磁盘不是sysinfo crate 读的。sysinfo 在这个项目里只管 CPU 和内存;磁盘走的是 macOS 原生 df -H 命令:

// system_stats.rs(示意)
Command::new("df").arg("-H")...   // 磁盘,1000-base,天然对齐 Finder
// sysinfo 负责 cpu / memory

df -H 本身就是 1000 进制输出,所以和 Finder 显示的数字误差在 0.01GB 以内——用户打开 Finder 和看 Sprite,数字应该一样。公网 IP 则是另一回事:它是网络请求,不能本地直读,所以后端做了 5 分钟缓存(public_ip 获取逻辑里 < 300 秒走缓存),每天只产生 1–3 次请求,把不必要的网络开销压到最低。

前端 useSystemStats.ts 每 5 秒拉一次:

const timer = window.setInterval(refresh, 5000);

待机时系统监控模块 CPU 占用不到 0.5%,前端到 Rust 后端的 RPC 延迟多在 2–8ms。颜色预警由 loadClass 给出:

export function loadClass(percent: number): string {
  if (percent >= 80) return "load-high"; // 红:该处理了
  if (percent >= 50) return "load-mid";  // 琥珀:注意一下
  return "load-low";                      // 绿
}

50% 是"注意一下",80% 是"该处理了"——我在真实负载里观察过这两档的触发频率,和预期吻合:琥珀色不能老亮着(否则失去提醒意义),红色也不能半年不亮(否则起不到预警)。

顺带,悬浮窗也能当专注计时器:内置 usePomodoro + PomodoroTimer,工作 / 休息切换只给轻量提示,不弹窗抢焦点。只是按我现在的用法,这个番茄钟更像是定给 AI 们的——干一会休息一会,别把 CPU 一直顶在红区;至于是我在给它们定节奏,还是它们在给我定,就不细究了。

常用网页一击即达:快捷启动器

第二个碎事:常用的内部网页、文档、本机应用散落在书签栏、Dock、各个地方。Sprite 用一份 launchers.*.json 把它们收拢成悬浮窗里的一排入口——你想加什么入口,直接编辑本地的 JSON 文件就行

  • url 类:一键打开常用网页(GitHub、erishen.cn 等)
  • app 类:直接唤起本机应用(终端、VS Code、Finder)
  • script 类:跑本地脚本

你自己的入口直接写进 launchers.local.json(走 .gitignore 不提交,纯本地),默认布局放 launchers.public.json 可提交分享——既能自定私有入口,又不泄露个人配置。后端 launcher.rs 负责这几类命令的执行。入口多了之后,HudLinks.tsx 还提供实时关键词过滤:输入即筛、高亮匹配、Esc 一键清空。

后端怎么"按一下就打开"?launcher.rs 把三种 kind 映射到不同的本地命令,跨平台用 #[cfg(target_os)] 分流——macOS 走系统 open

// launcher.rs
#[tauri::command]
pub fn launch(kind: String, target: String) -> Result<(), String> {
    if target.trim().is_empty() { return Err("空的启动目标".into()); }
    match kind.as_str() {
        "app"    => launch_app(&target),     // macOS: `open -a "<App>"`
        "bundle" => launch_bundle(&target),  // macOS: `open -b "<bundle-id>"`
        "script" => run_script(&target),     // `/bin/sh -c "<script>"`
        other    => Err(format!("未知的 launch 类型: {other}")),
    }
}

#[cfg(target_os = "macos")]
fn launch_app(name: &str) -> Result<(), String> {
    Command::new("/usr/bin/open").arg("-a").arg(name).spawn()?;
    Ok(())
}

target 全来自你自己的 launchers.*.json,不是远程内容,所以这里不做沙箱隔离——它是你授权的本地快捷方式。

密码不再散落:本地密码本

第三个碎事:账号密码不该记在备忘录里。Sprite 带一个本地密码本(密码箱),在设置里以「🔐 自定义配置」添加——每条是一个自定义项:填一个按钮文案(如"GitHub 密码"),再填内容(value)。内容以密文形式存储:写入时由 crypto.ts 用 AES-GCM 加密后落进本地 localStorage,不落明文;若启用了主密码,还得先解锁密码箱才能看到内容。整本密码箱本地私有、不出网——不连任何服务器,复制时也走 1 秒忽略窗口、不进剪贴板历史。支持整体导出(导出的是密文,dataManagement.ts)便于备份。

整个应用还能设锁屏密码,闲置后自动锁定——旁人路过看到的是一块锁住的悬浮窗,而不是你的密钥。

一句话:遇到要稳妥保存的敏感凭据,优先走这个本地密码本,而不是丢进备忘录、塞进浏览器自动填充、或任何会"出网"的云同步——它本地私有、不出网,本就是为这类东西准备的。

随时问"关于本机"的问题:内置 LLM(核心)

第四个、也是最常用的一件事:脑子里冒出个关于本机的问题,想顺手问 AI。普通聊天工具答不了"现在谁在吃 CPU""内存为什么涨了",因为它读不到你的实时系统。Sprite 的内置 LLM 可以——它和后端跑在同一台机器上,后端随时有最新的 SystemStats

配置好 builtinApiKey / builtinBase 后,BuiltinPanel 随时能被叫出来,直接就当前系统状态提问。配置合并逻辑 mergedConfig.ts 里,config.llmConfigured 只在 builtinApiKeybuiltinBase 同时非空时才为真:

llmConfigured: !!(builtinApiKey && builtinBase),

这避免了没配置却显示"已连接"的误导状态。这是我最常用的入口:悬浮窗看一眼 → 顺手问一句 → 拿到基于实时数据的回答。聊过的对话也能留档——内置 LLM 和三种 Harness 面板都支持把记录一键导出成 Markdown 或 JSON(chatExport.ts 调 Rust 的 save_export_file 落进下载目录,也可直接复制到剪贴板)。

把 Agent 服务也收进来:多 Harness 接入

第五个碎事:我自己跑着好几套 Agent 服务(resolve-studiospring-harness,以及一套通用 harness 协议——平时跑的是 resolve-harness),平时想调它们得开各自的界面。Sprite 直接当它们的前端:

  • ResolvePanelresolve-studio 的 agent(带工具 + 沙箱,流式返回)
  • SpringPanelspring-harness 的 ReAct Agent(SSE 流:thinking → tool → answer → done
  • HarnessPanel 接通用 harness 协议的服务(harness_chat / harness_approve / harness_models,当前跑的是 resolve-harness)

三者各自独立配置:resolveBase :8787springBase :8080harnessBase :8899useBackends 在启动时分别打 resolve_health / spring_models / harness_health 探活,HUD 上用点亮 / 熄灭 / 检测中三态标在线。于是悬浮窗不只是一个监控器,而是我桌面上的"随身 Agent 控制台"。

Tauri 视角:常驻悬浮窗是怎么搭起来的

前面这些能力能收进"一块常驻小窗",Tauri v2 是骨架。下面挑几段真实源码,看它怎么把"常驻、透明、可唤起、能存盘"落到代码上。

1. 应用骨架(lib.rs

tauri::Builder 串起插件、invoke_handler(前端能调的所有 Rust 命令)、以及 setup 里的初始化。启动时把主窗口钉在显示器右上角,再注册全局热键、构建托盘:

// src-tauri/src/lib.rs(节选)
pub fn run() {
    tauri::Builder::default()
        .plugin(tauri_plugin_opener::init())
        .plugin(tauri_plugin_global_shortcut::Builder::new().build())
        .plugin(tauri_plugin_clipboard_manager::init())
        .invoke_handler(tauri::generate_handler![
            system_stats::system_stats,
            launcher::launch,
            export::save_export_file,
            // …resolve / spring / harness / panel / hotkey / keychain
        ])
        .setup(|app| {
            // 把主窗口钉在显示器右上角
            if let Some(window) = app.get_webview_window("main") {
                if let Some(monitor) = app.primary_monitor()? {
                    let scale = monitor.scale_factor();
                    let screen = monitor.size().to_logical::<f64>(scale);
                    let win = window.outer_size()?.to_logical::<f64>(scale);
                    window.set_position(tauri::LogicalPosition::new(
                        (screen.width - win.width).max(0.0), 0.0))?;
                }
            }
            // 注册全局热键、构建托盘、启动托盘轮询 …
            Ok(())
        })
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

2. 子窗口:透明无边框 + 置顶"补刀"(panel.rs

点开某个 Harness 面板时,会 WebviewWindowBuilder 起一个同样透明无边框的小窗口。always_on_top 在 macOS 上对某些窗口类型会"不生效",所以代码在 150ms 后强制再置顶一次:

// panel.rs(节选)
let window = tauri::WebviewWindowBuilder::new(&app, &label, WebviewUrl::App("index.html".into()))
    .inner_size(w, h)
    .position(x, y)
    .transparent(true)     // 无边框 + 半透明
    .always_on_top(true)
    .decorations(false)
    .shadow(false)
    .resizable(false)
    .build()?;
window.set_always_on_top(true);
window.show();
window.set_focus();
// macOS 上某些窗口类型置顶会"不生效",150ms 后强制再置顶一次
let window_clone = window.clone();
tauri::async_runtime::spawn(async move {
    tokio::time::sleep(Duration::from_millis(150)).await;
    let _ = window_clone.set_always_on_top(true);
    let _ = window_clone.show();
    let _ = window_clone.set_focus();
});

3. 托盘:后台轮询重建菜单(tray.rs

托盘菜单每 15 秒由后台任务重建一次——用 futures_util::join! 并发探活三个后端 + 拉一次系统状态,再 set_menu 写回:

// tray.rs(节选)
pub fn start_tray_poll(app: &tauri::AppHandle) {
    let app = app.clone();
    tauri::async_runtime::spawn(async move {
        loop {
            let (r, s, h, sys) = futures_util::join!(
                resolve_studio::resolve_health("http://127.0.0.1:8787".into()),
                spring_harness::spring_models("http://127.0.0.1:8080".into()),
                harness::harness_health("http://127.0.0.1:8899".into()),
                system_stats::system_stats(),
            );
            if let Some(tray) = app.tray_by_id("hud-tray") {
                tray.set_menu(Some(build_tray_menu(&app, /* 后端状态 + 系统资源 */)));
            }
            tokio::time::sleep(Duration::from_secs(15)).await;
        }
    });
}

4. 导出:一个最小的 Tauri 命令(export.rs

对话导出落到 ~/Downloads,本质上就是一个读环境变量拼路径、写文件的 #[tauri::command]

// export.rs(节选)
#[tauri::command]
pub async fn save_export_file(file_name: String, content: String) -> Result<String, String> {
    let home = std::env::var("HOME").map_err(|e| format!("获取用户主目录失败: {e}"))?;
    let downloads_dir = PathBuf::from(&home).join("Downloads");
    if !downloads_dir.exists() {
        fs::create_dir_all(&downloads_dir).map_err(|e| format!("创建下载目录失败: {e}"))?;
    }
    let file_path = downloads_dir.join(&file_name);
    fs::write(&file_path, content).map_err(|e| format!("写入文件失败: {e}"))?;
    let path_str = file_path
        .to_str()
        .ok_or_else(|| "文件路径转换失败".to_string())
        .to_string();
    Ok(path_str)
}

这几段拼起来,就是"常驻桌面小窗"的技术骨架:配置决定窗口长相、Builder 决定能调什么、子窗口决定多面板、托盘决定后台心跳、命令决定本地存盘。

顺手取用:剪贴板历史

还有一个每天反复发生、却没人专门管的碎事:刚复制的一段命令、一个 ID、一段报错,切到别处想用,原窗口早翻上去了。Sprite 顺手把剪贴板记下来——useClipboardHistory 每 1 秒轮询一次(从 500ms 调到 1s 是为了少占 CPU),保留最近 20 条文本,去重、可清空,面板里还能搜、点一下就复制回去。

关键点在于它尊重隐私:命中信用卡号、身份证、手机号、API Key / Token、AWS Key、JWT、PEM 私钥这类正则的内容会自动跳过、不进历史(SENSITIVE_PATTERNS);复制密码时前端还会打一个"忽略窗口"标记,确保主密码不会漏进历史。这跟密码箱"本地私有、不出网"是一套思路——常驻桌面的东西,越能读到敏感上下文,越得在本地把住关。所以真要稳妥地存凭据,别塞备忘录、也别依赖云端同步,优先走上面的本地密码本:它本地私有、不出网,才是账号密码和 API Key 该待的地方。

用了之后:前后对比

以前要做这几件事,得在活动监视器、浏览器标签页、备忘录、另一个 AI App、各 Agent 界面之间反复切。现在它们都在一块半透明小窗里:

  • 轻量:Tauri v2 打包约 25MB(前端 3MB + Rust 后端 12MB + Tauri 运行时 10MB),因为 macOS 复用系统 WebView 而非打包 Chromium,比同类 Electron 应用(通常 80–150MB)小一个数量级;内存稳定在 80–120MB,连续跑 24 小时无明显增长。
  • 常驻不扰:半透明、不抢焦点,系统监控待机 CPU 不到 0.5%。
  • 可扩展:内置 LLM + 多 Harness 接入,让悬浮窗从"看板"变成"能问、能调 Agent 的控制台"。

它没解决什么惊天动地的问题,只是把一天里几十次上下文切换,收进了一块你几乎感觉不到它存在的小屏。

源码导航

Rust 后端(src-tauri/src/

  • system_stats.rs — 系统监控数据采集(CPU/内存走 sysinfo,磁盘走原生 df -H,公网 IP 5 分钟缓存)
  • builtin.rs — 内置 LLM 对话命令
  • resolve.rs / resolve_studio.rs — resolve-harness / resolve-studio 接入命令
  • spring_harness.rs — spring-harness ReAct 接入命令
  • harness.rs — 通用 harness 协议接入(harness_chat / harness_approve / harness_models),当前实现为 resolve-harness
  • keychain.rs / utils/keychain.ts — LLM / Harness 的 API Key 封装与系统钥匙串读写(keychain_save / keychain_get / keychain_deletekeychain_available 探活)
  • launcher.rs — 快捷启动器(url / app / script)命令
  • panel.rs / tray.rs / hotkey.rs / settings.rs — 窗口、托盘、热键、设置
  • export.rs — 聊天记录导出落盘(save_export_file 存下载目录)

前端(src/

  • App.tsx — 主窗口、锁屏、后端健康检查编排、launchers.*.json 合并

  • useSystemStats.ts — 系统监控轮询(5 秒)

  • hooks/useBackends.ts — resolve/spring/harness 健康探测(resolve_health / spring_models / harness_health

  • BuiltinPanel.tsx — 内置 LLM 聊天窗

  • ResolvePanel.tsx / SpringPanel.tsx / HarnessPanel.tsx — 三种 Harness 聊天窗

  • utils/mergedConfig.ts — 配置合并(settings > .env > 默认)

  • utils/crypto.ts / hooks/useCustomItems.ts / components/CustomItemsManager.tsx — 密码本(自定义配置):内容 AES-GCM 加密存本地 localStorage、可选主密码加锁

  • useMinimize.ts — 整窗收成 360×36 常驻横条(MIN_W=360 / MIN_H=36Esc 还原)

  • hooks/useClipboardHistory.ts / components/ClipboardHistory.tsx — 剪贴板历史(轮询 1s、最近 20 条、敏感内容正则跳过;复制密码打 1s 忽略窗口)

  • utils/chatExport.ts — 对话导出 Markdown / JSON(exportChatMessages → Rust save_export_file

  • components/HudLinks.tsx — 快捷启动器渲染与实时关键词过滤

  • config/launchers.public.json — 可提交的公开快捷方式

  • 本项目的 GitHub 仓库:https://github.com/erishen/sprite

  • 可接入的相关 Harness 项目:

    • resolve-studio — 带工具 + 沙箱的 agent(resolveBase :8787)
    • spring-harness — PSE 三角色编排的 ReAct Agent(springBase :8080)
    • resolve-harness — 通用 harness 协议的参考实现:Python Agent 骨架,LangGraph 编排 + LiteLLM 模型路由 + 分层记忆 + Fast Path(harnessBase :8899)

可复现

如果你想在本地跑起来看这些数字,环境大致如下:

项目 版本/状态
OS macOS (Apple Silicon)
Tauri 版本 v2
前端 Vite + React 19 + TypeScript
包管理器 pnpm
后端依赖 sysinfo(CPU/内存)+ 原生 df -H(磁盘)

前端和后端跑在同一台机器上,所以 system_stats 的 RPC 延迟基本只包含进程间通信开销。几个关键常量都写在源码里,方便你核对:

  • 刷新间隔:useSystemStats.tswindow.setInterval(refresh, 5000)
  • 公网 IP 缓存:system_stats.rs< 300 秒走缓存
  • 颜色阈值:useSystemStats.tsloadClass(50% / 80%)
  • 后端端口:mergedConfig.tsresolveBase :8787 / springBase :8080 / harnessBase :8899

打包体积用 pnpm tauri build 后看输出目录;内存占用用系统监视器看进程 RSS,启动后稳定在 80–120MB。磁盘精度对照最简单:终端跑 df -H,和悬浮窗里的 disk_used_gb / disk_total_gb 四舍五入到两位小数应当吻合。

Sprite 的 README 写「polled every 3s」,但代码实际是 5 秒刷新一次,这个差异合理吗?

合理。5 秒间隔是实测后的设计取舍:前端到 Rust 后端的 RPC 延迟仅 2-8ms,系统监控模块待机 CPU 占用不到 0.5%,用户几乎无感知延迟。3s 文档是初始规划值,5s 是后来的优化结果,在刷新及时性和资源开销之间取得平衡。

磁盘使用量如何保证与 macOS Finder 一致?

后端对磁盘直接调用 macOS 原生 df -H 命令读取(sysinfo crate 只负责 CPU/内存),df -H 输出天然就是 1000-base,因此与 Finder 显示误差在 0.01GB 以内。这是精度与兼容性的取舍——沿用系统命令的 1000-base 而非 1024-base,确保数字与用户熟悉的系统工具对齐。

配置合并的优先级策略是什么?

settings JSON 文件 > .env 文件 > 代码默认值。空值会正确回退,多后端配置(Resolve Harness :8899、Spring Harness :8080、Resolve Studio :8787、内置 LLM)各自独立合并。内置 LLM 仅在 builtinApiKeybuiltinBase 同时非空时才标记为已配置,避免显示误导性状态。

公网 IP 为什么有 5 分钟缓存?

这是网络开销与数据新鲜度的取舍。实测表明,5 分钟缓存可将公网 IP 请求降至每天 1-3 次,大幅减少不必要的网络消耗。缓存失效时用户无感知,因为 IP 变化频率远低于 5 分钟。

系统负载的颜色阈值(50% 琥珀色、80% 红色)是如何确定的?

基于用户体验观察的经验阈值。50% 是「开始注意」的温和提醒点,80% 是「需要干预」的明确警告点。大多数用户可容忍 50% 以下负载不感知卡顿,超过 80% 时系统已明显变慢。这两个分界点在视觉预警和误报之间取得平衡。

Sprite 的打包体积和内存占用表现如何?

Tauri v2 打包后约 25MB(前端资源约 3MB、Rust 后端约 12MB、Tauri 运行时约 10MB),因为 macOS 上复用系统 WebView 而非打包 Chromium,比同类 Electron 应用(通常 80–150MB)小一个数量级。内存稳定在 80–120MB,连续运行 24 小时无明显增长,说明没有严重内存泄漏;系统监控模块待机 CPU 占用不到 0.5%,前端到 Rust 后端的 RPC 延迟多在 2–8ms。这些均为我真实工作环境下的测量值,样本有限,仅作基线参考。

Sprite 支持接入哪些 Harness 服务?如何判断它们在线?

Sprite 内置四种对话后端:内置 LLM(BuiltinPanel)、resolve-studio(ResolvePanel,agent 带工具 + 沙箱,流式返回)、spring-harness(SpringPanel,ReAct Agent,SSE 流 thinking → tool → answer → done)、通用 harness 协议(HarnessPanel,任何实现该协议的服务都能接,同步 /api/chat 返回 reply + 工具调用 trace,并支持 harness_approve / harness_models;当前跑的是 resolve-harness)。各后端独立配置 resolveBase :8787 / springBase :8080 / harnessBase :8899useBackends 在启动时分别打 resolve_health / spring_models / harness_health 探活,HUD 上以点亮 / 熄灭 / 检测中三态标示在线状态。llmConfigured 仅当 builtinApiKeybuiltinBase 同时非空才为真,避免误导性的「已连接」。

密码本(密码箱)的数据如何保证安全?

密码本在设置里是「自定义配置」,每条填一个按钮文案 + 内容(value)。内容以密文存储:写入时由 crypto.ts 用 AES-GCM 加密后落进本地 localStorage,不落明文;导出(dataManagement.ts)出去的也是密文。若启用了主密码,还得先解锁密码箱才能看到内容。复制密码时还会打 1 秒忽略窗口,不进剪贴板历史。整本本地私有、不出网。此外整个应用可设锁屏密码,闲置后自动锁定,避免旁人路过看到悬浮窗内容。

注:LLM / Harness 的 API Key 才走 keychain.rs 存进 macOS 系统钥匙串;密码箱的条目走的是上面这套本地加密存储,两者是分开的。

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

商店 Web Chat Nsbp 关于 隐私政策

@ 2026 ESN
沪ICP备2024079226号-1   沪公网安备31010502007082号