引言
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.ts:MIN_W=360、MIN_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 只在 builtinApiKey 和 builtinBase 同时非空时才为真:
llmConfigured: !!(builtinApiKey && builtinBase),
这避免了没配置却显示"已连接"的误导状态。这是我最常用的入口:悬浮窗看一眼 → 顺手问一句 → 拿到基于实时数据的回答。聊过的对话也能留档——内置 LLM 和三种 Harness 面板都支持把记录一键导出成 Markdown 或 JSON(chatExport.ts 调 Rust 的 save_export_file 落进下载目录,也可直接复制到剪贴板)。
把 Agent 服务也收进来:多 Harness 接入
第五个碎事:我自己跑着好几套 Agent 服务(resolve-studio、spring-harness,以及一套通用 harness 协议——平时跑的是 resolve-harness),平时想调它们得开各自的界面。Sprite 直接当它们的前端:
ResolvePanel接 resolve-studio 的 agent(带工具 + 沙箱,流式返回)SpringPanel接 spring-harness 的 ReAct Agent(SSE 流:thinking → tool → answer → done)HarnessPanel接通用 harness 协议的服务(harness_chat/harness_approve/harness_models,当前跑的是 resolve-harness)
三者各自独立配置:resolveBase :8787、springBase :8080、harnessBase :8899。useBackends 在启动时分别打 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-harnesskeychain.rs/utils/keychain.ts— LLM / Harness 的 API Key 封装与系统钥匙串读写(keychain_save/keychain_get/keychain_delete,keychain_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=36,Esc还原)hooks/useClipboardHistory.ts/components/ClipboardHistory.tsx— 剪贴板历史(轮询 1s、最近 20 条、敏感内容正则跳过;复制密码打 1s 忽略窗口)utils/chatExport.ts— 对话导出 Markdown / JSON(exportChatMessages→ Rustsave_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.ts的window.setInterval(refresh, 5000) - 公网 IP 缓存:
system_stats.rs里< 300秒走缓存 - 颜色阈值:
useSystemStats.ts的loadClass(50% / 80%) - 后端端口:
mergedConfig.ts的resolveBase :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 仅在 builtinApiKey 和 builtinBase 同时非空时才标记为已配置,避免显示误导性状态。
公网 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 :8899;useBackends 在启动时分别打 resolve_health / spring_models / harness_health 探活,HUD 上以点亮 / 熄灭 / 检测中三态标示在线状态。llmConfigured 仅当 builtinApiKey 与 builtinBase 同时非空才为真,避免误导性的「已连接」。
密码本(密码箱)的数据如何保证安全?
密码本在设置里是「自定义配置」,每条填一个按钮文案 + 内容(value)。内容以密文存储:写入时由 crypto.ts 用 AES-GCM 加密后落进本地 localStorage,不落明文;导出(dataManagement.ts)出去的也是密文。若启用了主密码,还得先解锁密码箱才能看到内容。复制密码时还会打 1 秒忽略窗口,不进剪贴板历史。整本本地私有、不出网。此外整个应用可设锁屏密码,闲置后自动锁定,避免旁人路过看到悬浮窗内容。
注:LLM / Harness 的 API Key 才走 keychain.rs 存进 macOS 系统钥匙串;密码箱的条目走的是上面这套本地加密存储,两者是分开的。
发表回复