OpenHuman 深度实战:从 Tauri 2.x 到桌面 AI 超级智能体——2026 年个人 AI 助手架构完全指南
当 OpenClaw(龙虾)和 Hermes Agent(爱马仕)还在后端默默耕耘时,OpenHuman(人类)已经冲到了台前——一个周末破万 Star, Product Hunt 霸榜,硅谷热议。它凭什么?本文将从架构设计、技术栈选型、核心功能实现、安全考量等维度,深度剖析这款基于 Tauri 2.x + Rust + TypeScript 的桌面 AI 智能体,并带你从零构建一个最小化可用的 OpenHuman 插件。
目录
- 背景介绍:为什么我们需要桌面级 AI 智能体?
- OpenHuman 的核心设计哲学
- 技术栈深度解析:Tauri 2.x + Rust + TypeScript
- 架构分析:从前端 UI 到 Rust 核心层
- 核心功能实战
- 性能优化:Tauri + CEF 架构的内存与速度优势
- 安全考量:本地化存储与权限控制
- 从零构建 OpenHuman 插件:完整实战
- 总结与展望:桌面 AI 智能体的未来
1. 背景介绍:为什么我们需要桌面级 AI 智能体?
1.1 现有 AI 助手的局限
2026 年的今天,AI 助手已经渗透到我们工作和生活的方方面面。但从产品形态来看,绝大多数 AI 助手仍然存在以下局限:
1. 缺乏持久化记忆
无论是 ChatGPT、Claude 还是 Gemini,它们的记忆机制都依赖于会话上下文(Context Window)。一旦会话结束,或者上下文窗口溢出,之前的对话内容就会丢失。虽然部分产品(如 Claude 的 Memory 功能)支持跨会话记忆,但:
- 记忆内容由 AI 自动提取,用户无法精确控制
- 记忆存储在云端,存在隐私泄露风险
- 记忆容量受限,无法承载用户的全部数字足迹
2. 无法访问本地数据与应用程序
现有的 AI 助手大多运行在云端,无法直接访问用户的本地文件、应用程序和数据。即使用户通过上传文件、复制粘贴等方式提供上下文,也存在以下痛点:
- 手动操作繁琐,效率低下
- 文件大小受限,无法处理大规模数据
- 无法实时同步本地数据的变化
3. 缺乏主动感知能力
现有的 AI 助手大多是"被动响应"模式——用户问,AI 答。它们无法主动感知用户的状态、环境和需求,更无法在合适的时机提供主动建议。
1.2 OpenHuman 的破局之道
OpenHuman 由 TinyHumans AI 团队开发,其设计哲学是:
一个 AI 助手只有具备了用户的上下文信息,才能真正发挥作用。
基于此,OpenHuman 提出了以下解决方案:
- 本地化记忆系统(Memory Tree):将用户的上下文信息存储在本地 SQLite 数据库中,并构建成知识图谱,用户可随时查看、编辑和导出(兼容 Obsidian)。
- 深度第三方集成(118+ Integrations):通过 OAuth 和 API 调用,连接 Gmail、Notion、Slack、Google Calendar 等常用工具,实现数据自动同步。
- 主动感知与自动化(Desktop Pet + Autopilot):在桌面显示可交互的 AI 宠物,并支持定时任务、事件触发等自动化能力。
- 智能上下文压缩(TokenJuice):在有限的 Context Window 内,通过智能算法压缩和提取关键信息,最大化利用每一次 AI 调用。
2. OpenHuman 的核心设计哲学
2.1 "了解你"比"回答你"更重要
OpenHuman 的核心设计哲学是:AI 助手的价值不在于"回答你的问题",而在于"了解你是谁"。
这句话听起来有点玄乎,但其实很好理解。举个例子:
场景 1:传统 AI 助手
用户:帮我安排一下明天的会议。
AI:好的,请问您明天有哪些会议需要安排?
用户:哦,我忘了,你帮我看看日历。
AI:抱歉,我无法直接访问您的日历。请您手动查看后告诉我。
场景 2:OpenHuman
用户:帮我安排一下明天的会议。
AI:好的,我已经查看了您的 Google Calendar,明天有以下会议:
- 09:00 - 10:00 Standup Meeting
- 14:00 - 15:30 Client Demo
您希望我帮您做什么调整吗?
这个例子可能还不够震撼。我们再举一个更复杂的场景:
场景 3:传统 AI 助手
用户:帮我写一封邮件,向客户介绍我们的新产品。
AI:好的,请您提供以下信息:
1. 产品名称和主要功能
2. 目标客户群体
3. 您的公司背景
...
用户:(内心 OS:我之前不是跟你说过吗?怎么又忘了?)
场景 4:OpenHuman
用户:帮我写一封邮件,向客户介绍我们的新产品。
AI:好的,根据您之前提供给我的信息,我知道您的公司是一家 B2B SaaS 企业,
产品是"智分析"——一款面向中小企业的数据分析平台。
目标客户是年营收 500 万至 2000 万的中小企业主。
我已经帮您起草了一封邮件,请您审阅:
...
核心差异:OpenHuman 通过 Memory Tree 和第三方集成,持续积累关于用户的信息,并在需要的时候自动调用,而无需用户反复提供。
2.2 开源 + 本地化 = 隐私保护
OpenHuman 采用 GPL-3.0 许可证开源,并且所有用户数据都存储在本地(SQLite + Obsidian Vault)。这意味着:
- 用户拥有数据的完全控制权:可以随时导出、删除或迁移数据。
- 无需担心云端隐私泄露:数据不经过任何第三方服务器。
- 可审计性:开源代码意味着任何人都可以审查其数据处理逻辑,确保没有后门或恶意行为。
当然,本地化也带来了一些挑战,比如:
- 跨设备同步困难:本地数据无法直接在不同设备间同步。
- 计算资源受限:依赖本地算力,无法利用云端 GPU 集群。
OpenHuman 通过以下方式缓解这些问题:
- Obsidian Vault 同步:用户可以借助 Obsidian 的同步功能(或第三方同步工具)实现跨设备数据同步。
- 混合推理模式:支持调用云端 AI 模型(如 OpenAI API、Anthropic API)进行推理,同时保留本地记忆系统。
3. 技术栈深度解析:Tauri 2.x + Rust + TypeScript
3.1 为什么选择 Tauri 而不是 Electron?
如果你曾经开发过桌面应用,那么你一定听说过 Electron。Electron 是一种基于 Web 技术(HTML/CSS/JavaScript)构建桌面应用的框架,知名产品如 VS Code、Slack、Discord 都基于 Electron 构建。
但 Electron 有一个广受诟病的问题:资源占用高。
一个最简单的 Electron 应用,打包后通常也有 100MB+,运行时内存占用更是轻松突破 200MB。这对于一个需要长期后台运行的 AI 助手来说,显然是不可接受的。
Tauri 的优势:
| 对比维度 | Electron | Tauri 2.x |
|---|---|---|
| 打包体积 | 100MB+ | 3-10MB |
| 内存占用 | 200MB+ | 30-80MB |
| 渲染引擎 | Chromium(内置) | 系统 WebView(Windows: WebView2, macOS: WKWebView, Linux: WebKitGTK) |
| 后端语言 | Node.js (JavaScript) | Rust |
| 性能 | 一般 | 高(Rust 零成本抽象) |
| 安全性 | 一般(Node.js 沙箱逃逸风险) | 高(Rust 内存安全 + 权限控制) |
Tauri 2.x 是 Tauri 的最新版本,相较于 1.x,它带来了以下重大改进:
- 移动端支持:Tauri 2.x 支持构建 iOS 和 Android 应用,真正实现"一次编写,多端运行"。
- 插件系统重构:插件 API 更加规范化和易用,方便社区贡献。
- IPC 性能优化:前后端通信(Invoke)性能提升约 40%。
3.2 Rust 核心层的职责
在 Tauri 架构中,前端(UI)使用 Web 技术(React + TypeScript)构建,运行在系统 WebView 中;后端(Core)使用 Rust 构建,负责处理计算密集型任务和系统级操作。
OpenHuman 的 Rust 核心层主要承担以下职责:
1. 系统级 API 调用
- 文件系统访问(读取/写入用户文件)
- 进程管理(启动/停止本地服务)
- 网络通信(HTTP 请求、WebSocket 连接)
- 系统通知(桌面通知、托盘图标)
2. 数据处理与计算
- Memory Tree 的构建与查询(图数据库操作)
- TokenJuice 压缩算法(自然语言处理)
- 第三方 API 签名与加密(OAuth Token 管理)
3. 安全隔离
- 沙箱化第三方集成(防止恶意插件窃取数据)
- 权限控制(用户可精确控制每个插件的权限)
3.3 TypeScript 前端的交互设计
OpenHuman 的前端使用 React + TypeScript 构建,主要承担以下职责:
1. 用户界面渲染
- 主窗口:聊天界面、设置页面、Memory Tree 可视化
- 托盘菜单:快速操作、状态显示
- Desktop Pet:可交互的桌面宠物(可选)
2. 状态管理
- 使用 Redux Toolkit 或 Zustand 管理全局状态
- 与 Rust 核心层通过 Tauri API(
invoke、listen、emit)进行通信
3. 实时更新
- 使用 WebSocket 或 Server-Sent Events (SSE) 接收后端推送(如任务进度、新邮件通知)
4. 架构分析:从前端 UI 到 Rust 核心层
4.1 整体架构图
+-----------------------+
| OpenHuman 桌面应用 |
| (Tauri 2.x) |
+-----------+-------------+---------------------+-----------+
| | |
v v v
+-----------+---+ +-----+------+ +-------+------+
| 前端 (React) | | Rust 核心层 | | 系统 WebView |
| | | | | |
| - 聊天界面 | | - Memory | | - Windows: |
| - 设置页面 | | Tree | | WebView2 |
| - Memory | | - TokenJuice| | - macOS: |
| Tree 可视化| | - 第三方 | | WKWebView |
| - Desktop | | 集成 | | - Linux: |
| Pet | | - 安全隔离 | | WebKitGTK |
+-----------+-+ +------+------+ +--------------+
| |
| v
| +--------+--------+
| | 本地数据存储 |
| | |
| | - SQLite |
| | (Memory Tree)|
| | - Obsidian |
| | Vault |
| +----------------+
|
v
+-----------+-----------+
| 第三方服务集成 |
| |
| - Gmail (OAuth2) |
| - Notion (Token) |
| - Slack (OAuth2) |
| - Google Calendar |
| - ... (118+) |
+-----------------------+
4.2 前后端通信机制
Tauri 提供了多种前后端通信机制:
1. invoke:前端调用后端
前端通过 invoke 函数调用后端的 Rust 命令:
// frontend/src/commands/memory.ts
import { invoke } from '@tauri-apps/api/core';
// 保存一条记忆
export async function saveMemory(content: string, tags: string[]) {
try {
const result = await invoke('save_memory', { content, tags });
return result;
} catch (error) {
console.error('Failed to save memory:', error);
throw error;
}
}
// 搜索记忆
export async function searchMemory(query: string, limit: number = 10) {
try {
const results = await invoke('search_memory', { query, limit });
return results;
} catch (error) {
console.error('Failed to search memory:', error);
throw error;
}
}
对应的 Rust 后端实现:
// src-tauri/src/commands/memory.rs
use serde::{Deserialize, Serialize};
use tauri::State;
use crate::memory::MemoryTree;
#[derive(Debug, Serialize, Deserialize)]
pub struct SaveMemoryRequest {
pub content: String,
pub tags: Vec<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct SearchMemoryRequest {
pub query: String,
pub limit: usize,
}
#[tauri::command]
pub async fn save_memory(
state: State<'_, AppState>,
request: SaveMemoryRequest,
) -> Result<String, String> {
let memory_tree = &state.memory_tree;
let id = memory_tree
.save(request.content, request.tags)
.await
.map_err(|e| format!("Failed to save memory: {}", e))?;
Ok(id)
}
#[tauri::command]
pub async fn search_memory(
state: State<'_, AppState>,
request: SearchMemoryRequest,
) -> Result<Vec<MemoryEntry>, String> {
let memory_tree = &state.memory_tree;
let results = memory_tree
.search(&request.query, request.limit)
.await
.map_err(|e| format!("Failed to search memory: {}", e))?;
Ok(results)
}
2. listen / emit:后端向前端推送事件
后端可以通过 emit 向前端发送事件,前端通过 listen 监听事件:
// frontend/src/events.ts
import { listen } from '@tauri-apps/api/event';
export function setupEventListeners() {
// 监听内存保存成功事件
listen('memory_saved', (event) => {
const payload = event.payload as { id: string; content: string };
console.log('Memory saved:', payload);
// 更新 UI
});
// 监听第三方集成同步进度事件
listen('integration_sync_progress', (event) => {
const payload = event.payload as { integration: string; progress: number };
console.log(`Syncing ${payload.integration}: ${payload.progress * 100}%`);
// 更新进度条
});
}
对应的 Rust 后端实现:
// src-tauri/src/integrations/sync.rs
use tauri::{AppHandle, Manager};
pub struct IntegrationSync {
app_handle: AppHandle,
}
impl IntegrationSync {
pub fn new(app_handle: AppHandle) -> Self {
Self { app_handle }
}
pub async fn sync_gmail(&self) -> Result<(), String> {
// 模拟同步过程
for progress in [0.1, 0.3, 0.6, 0.9, 1.0] {
self.app_handle
.emit("integration_sync_progress", serde_json::json!({
"integration": "gmail",
"progress": progress
}))
.map_err(|e| format!("Failed to emit event: {}", e))?;
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
}
Ok(())
}
}
4.3 State Management(状态管理)
在 Tauri 中,可以使用 State 在 Rust 命令之间共享数据。OpenHuman 使用 AppState 结构体来统一管理应用状态:
// src-tauri/src/state.rs
use std::sync::Arc;
use tokio::sync::RwLock;
use crate::memory::MemoryTree;
use crate::integrations::IntegrationManager;
use crate::token_juice::TokenJuice;
pub struct AppState {
pub memory_tree: Arc<RwLock<MemoryTree>>,
pub integration_manager: Arc<RwLock<IntegrationManager>>,
pub token_juice: Arc<TokenJuice>,
}
impl AppState {
pub fn new() -> Self {
Self {
memory_tree: Arc::new(RwLock::new(MemoryTree::new())),
integration_manager: Arc::new(RwLock::new(IntegrationManager::new())),
token_juice: Arc::new(TokenJuice::new()),
}
}
}
在 Tauri 应用启动时,将 AppState 注册为状态:
// src-tauri/src/main.rs
mod commands;
mod state;
mod memory;
mod integrations;
mod token_juice;
use tauri::Manager;
use state::AppState;
fn main() {
let app_state = AppState::new();
tauri::Builder::default()
.manage(app_state) // 注册状态
.invoke_handler(tauri::generate_handler![
commands::memory::save_memory,
commands::memory::search_memory,
// ... 其他命令
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
5. 核心功能实战
5.1 118+ 第三方集成:OAuth 与 API 调用
OpenHuman 支持 118+ 第三方集成,涵盖邮件、日历、笔记、项目管理、社交媒体等各类工具。这一节我们以 Gmail 集成为例,讲解如何实现 OAuth 2.0 授权和 API 调用。
5.1.1 OAuth 2.0 授权流程
Gmail API 使用 OAuth 2.0 进行授权。整个流程如下:
+---------+ +-------------------+ +---------------+
| | | | | |
| User | | OpenHuman | | Google OAuth |
| | | (Desktop App) | | Server |
| | | | | |
+---------+ +-------------------+ +---------------+
| | |
| 1. 点击"连接 Gmail" | |
|---------------------------------->| |
| | 2. 生成授权 URL |
| |---------------------------------->|
| | |
| | 3. 返回授权 URL |
| |<----------------------------------|
| | |
| 4. 打开系统浏览器, | |
| 跳转至授权 URL | |
|<----------------------------------| |
| | |
| 5. 用户登录并授权 | |
|---------------------------------->| |
| | |
| 6. Google 重定向至回调 URL (localhost:xxxx) |
|<----------------------------------| |
| | |
| 7. 本地 HTTP 服务器接收授权码 | |
|---------------------------------->| |
| | |
| | 8. 使用授权码交换访问令牌 |
| |---------------------------------->|
| | |
| | 9. 返回访问令牌 |
| |<----------------------------------|
| | |
| 10. 保存访问令牌(加密存储) |
| | |
| 11. 显示"已连接"状态 | |
|<----------------------------------| |
5.1.2 代码实现
前端:触发 OAuth 授权
// frontend/src/integrations/gmail.ts
import { invoke } from '@tauri-apps/api/core';
export async function connectGmail() {
try {
// 调用后端命令,获取授权 URL
const authUrl = await invoke('gmail_get_auth_url');
// 打开系统浏览器,跳转至授权 URL
await open(authUrl);
// 监听授权完成事件(后端会在接收到回调后 emit 事件)
const unlisten = await listen('gmail_auth_complete', async (event) => {
const { success, error } = event.payload as { success: boolean; error?: string };
if (success) {
console.log('Gmail connected successfully!');
// 更新 UI 状态
} else {
console.error('Failed to connect Gmail:', error);
}
unlisten();
});
} catch (error) {
console.error('Failed to connect Gmail:', error);
}
}
后端:OAuth 2.0 实现(Rust)
// src-tauri/src/integrations/gmail/oauth.rs
use oauth2::{AuthUrl, ClientId, ClientSecret, RedirectUrl, Scope, TokenUrl};
use oauth2::basic::BasicClient;
use oauth2::reqwest::async_http_client;
use serde::{Deserialize, Serialize};
use tauri::{State, AppHandle, Manager};
use std::net::SocketAddr;
use axum::{Router, Server};
use axum::extract::Query;
use tower::ServiceExt;
use crate::state::AppState;
#[derive(Debug, Serialize, Deserialize)]
pub struct GmailAuthConfig {
pub client_id: String,
pub client_secret: String,
pub redirect_port: u16,
}
#[tauri::command]
pub async fn gmail_get_auth_url(
state: State<'_, AppState>,
) -> Result<String, String> {
let config = GmailAuthConfig {
client_id: std::env::var("GMAIL_CLIENT_ID").map_err(|_| "Missing GMAIL_CLIENT_ID")?,
client_secret: std::env::var("GMAIL_CLIENT_SECRET").map_err(|_| "Missing GMAIL_CLIENT_SECRET")?,
redirect_port: 9876, // 本地回调端口
};
// 创建 OAuth2 客户端
let client = BasicClient::new(ClientId::new(config.client_id))
.set_client_secret(ClientSecret::new(config.client_secret))
.set_auth_url(AuthUrl::new("https://accounts.google.com/o/oauth2/v2/auth".to_string()).unwrap())
.set_token_url(TokenUrl::new("https://oauth2.googleapis.com/token".to_string()).unwrap())
.set_redirect_url(RedirectUrl::new(format!("http://localhost:{}", config.redirect_port)).unwrap());
// 生成授权 URL
let (auth_url, csrf_token) = client
.authorize_url(oauth2::CsrfToken::new_random)
.add_scope(Scope::new("https://www.googleapis.com/auth/gmail.readonly".to_string()))
.url();
// 保存 CSRF Token(防止跨站请求伪造)
state.integration_manager.write().await.save_csrf_token("gmail", csrf_token.secret());
Ok(auth_url.to_string())
}
// 本地 HTTP 服务器,用于接收 OAuth 回调
pub async fn start_oauth_callback_server(
app_handle: AppHandle,
port: u16,
) -> Result<(), String> {
let app = Router::new()
.route("/callback", axum::routing::get(oauth_callback_handler))
.layer(tower::layer::util::Identity::new());
let addr = SocketAddr::from(([127, 0, 0, 1], port));
Server::bind(&addr)
.serve(app.into_make_service())
.await
.map_err(|e| format!("Failed to start OAuth callback server: {}", e))?;
Ok(())
}
// OAuth 回调处理函数
async fn oauth_callback_handler(
Query(params): Query<std::collections::HashMap<String, String>>,
) -> String {
let code = params.get("code").cloned().unwrap_or_default();
let state = params.get("state").cloned().unwrap_or_default();
// 验证 state(CSRF Token)
// ...
// 使用授权码交换访问令牌
// ...
// 保存访问令牌(加密存储)
// ...
// 通知前端授权完成
app_handle.emit("gmail_auth_complete", serde_json::json!({
"success": true
})).unwrap();
"授权完成!您可以关闭此窗口。".to_string()
}
5.1.3 Gmail API 调用示例
授权完成后,就可以调用 Gmail API 读取用户的邮件了:
// src-tauri/src/integrations/gmail/api.rs
use reqwest::header;
use serde_json::Value;
use crate::integrations::TokenStorage;
pub struct GmailApi {
client: reqwest::Client,
token_storage: TokenStorage,
}
impl GmailApi {
pub fn new(token_storage: TokenStorage) -> Self {
Self {
client: reqwest::Client::new(),
token_storage,
}
}
// 列出收件箱邮件
pub async fn list_messages(&self, max_results: u32) -> Result<Vec<Message>, String> {
let access_token = self.token_storage.get_token("gmail").await?;
let response = self.client
.get("https://gmail.googleapis.com/gmail/v1/users/me/messages")
.header(header::AUTHORIZATION, format!("Bearer {}", access_token))
.query(&[("maxResults", max_results.to_string())])
.send()
.await
.map_err(|e| format!("Failed to call Gmail API: {}", e))?;
let json: Value = response.json().await.map_err(|e| format!("Failed to parse response: {}", e))?;
// 解析响应
let messages = json["messages"]
.as_array()
.unwrap_or(&vec![])
.iter()
.map(|m| Message {
id: m["id"].as_str().unwrap_or_default().to_string(),
thread_id: m["threadId"].as_str().unwrap_or_default().to_string(),
})
.collect();
Ok(messages)
}
// 获取邮件详情
pub async fn get_message(&self, message_id: &str) -> Result<MessageDetail, String> {
let access_token = self.token_storage.get_token("gmail").await?;
let response = self.client
.get(format!("https://gmail.googleapis.com/gmail/v1/users/me/messages/{}", message_id))
.header(header::AUTHORIZATION, format!("Bearer {}", access_token))
.send()
.await
.map_err(|e| format!("Failed to call Gmail API: {}", e))?;
let json: Value = response.json().await.map_err(|e| format!("Failed to parse response: {}", e))?;
// 解析邮件详情
let detail = MessageDetail {
id: json["id"].as_str().unwrap_or_default().to_string(),
subject: json["payload"]["headers"]
.as_array()
.unwrap_or(&vec![])
.iter()
.find(|h| h["name"].as_str() == Some("Subject"))
.and_then(|h| h["value"].as_str())
.unwrap_or_default()
.to_string(),
from: json["payload"]["headers"]
.as_array()
.unwrap_or(&vec![])
.iter()
.find(|h| h["name"].as_str() == Some("From"))
.and_then(|h| h["value"].as_str())
.unwrap_or_default()
.to_string(),
body: extract_body_from_payload(&json["payload"]),
};
Ok(detail)
}
}
// 从 Gmail API 的 payload 中提取邮件正文
fn extract_body_from_payload(payload: &Value) -> String {
if let Some(body) = payload["body"]["data"].as_str() {
// Base64 解码
let decoded = base64::decode(body).unwrap_or_default();
return String::from_utf8(decoded).unwrap_or_default();
}
if let Some(parts) = payload["parts"].as_array() {
for part in parts {
if part["mimeType"].as_str() == Some("text/plain") {
if let Some(body) = part["body"]["data"].as_str() {
let decoded = base64::decode(body).unwrap_or_default();
return String::from_utf8(decoded).unwrap_or_default();
}
}
}
}
String::new()
}
5.2 Memory Tree:本地知识图谱构建
Memory Tree 是 OpenHuman 的核心功能之一。它负责将用户的上下文信息(对话历史、文件内容、第三方集成数据)构建成一个本地化的知识图谱,并在需要的时候进行快速检索。
5.2.1 数据模型设计
Memory Tree 使用 SQLite 作为存储引擎,并通过树形结构(实际上是一个图)来组织记忆节点。每个记忆节点包含以下字段:
CREATE TABLE memories (
id TEXT PRIMARY KEY, -- 唯一标识符 (UUID)
content TEXT NOT NULL, -- 记忆内容 (文本)
content_type TEXT NOT NULL, -- 内容类型 (conversation, file, integration, etc.)
source TEXT, -- 来源 (user, gmail, notion, etc.)
created_at INTEGER NOT NULL, -- 创建时间 (Unix 时间戳)
updated_at INTEGER NOT NULL, -- 更新时间 (Unix 时间戳)
embedding BLOB, -- 向量嵌入 (用于语义搜索)
metadata TEXT -- 元数据 (JSON 格式)
);
CREATE TABLE memory_edges (
id INTEGER PRIMARY KEY AUTOINCREMENT,
source_id TEXT NOT NULL, -- 源节点 ID
target_id TEXT NOT NULL, -- 目标节点 ID
relationship TEXT NOT NULL, -- 关系类型 (related_to, belongs_to, etc.)
weight REAL DEFAULT 1.0, -- 关系权重
created_at INTEGER NOT NULL,
FOREIGN KEY (source_id) REFERENCES memories(id) ON DELETE CASCADE,
FOREIGN KEY (target_id) REFERENCES memories(id) ON DELETE CASCADE
);
CREATE TABLE memory_tags (
id INTEGER PRIMARY KEY AUTOINCREMENT,
memory_id TEXT NOT NULL,
tag TEXT NOT NULL,
FOREIGN KEY (memory_id) REFERENCES memories(id) ON DELETE CASCADE
);
CREATE INDEX idx_memories_content ON memories(content);
CREATE INDEX idx_memories_source ON memories(source);
CREATE INDEX idx_memories_created_at ON memories(created_at);
CREATE INDEX idx_memory_edges_source ON memory_edges(source_id);
CREATE INDEX idx_memory_edges_target ON memory_edges(target_id);
CREATE INDEX idx_memory_tags_memory_id ON memory_tags(memory_id);
CREATE INDEX idx_memory_tags_tag ON memory_tags(tag);
5.2.2 向量嵌入与语义搜索
为了实现语义搜索(即使用自然语言表达的查询,而不是精确的关键词匹配),Memory Tree 使用向量嵌入(Embedding)技术。
具体来说,每当保存一条新记忆时,OpenHuman 会调用本地或云端的 Embedding 模型(如 all-MiniLM-L6-v2 或 OpenAI text-embedding-3-small),将记忆内容转换为一个高维向量(例如 384 维或 1536 维),并存储在 memories.embedding 字段中。
在进行语义搜索时,OpenHuman 会将用户的查询也转换为向量,然后计算查询向量与所有记忆向量之间的余弦相似度(Cosine Similarity),并按相似度排序返回结果。
代码实现:
// src-tauri/src/memory/embedding.rs
use std::sync::Arc;
use candle::{Device, Tensor};
use candle_nn::VarBuilder;
use candle_transformers::models::bert::{BertModel, Config};
use tokenizers::Tokenizer;
use crate::state::AppState;
pub struct EmbeddingModel {
model: BertModel,
tokenizer: Tokenizer,
device: Device,
}
impl EmbeddingModel {
pub fn load(model_path: &str, tokenizer_path: &str) -> Result<Self, String> {
let device = Device::Cpu; // 如果有 GPU,可以使用 Device::Cuda(0)
// 加载分词器
let tokenizer = Tokenizer::from_file(tokenizer_path)
.map_err(|e| format!("Failed to load tokenizer: {}", e))?;
// 加载模型权重
let config = Config::from_file(format!("{}/config.json", model_path))
.map_err(|e| format!("Failed to load model config: {}", e))?;
let vb = VarBuilder::from_pth(format!("{}/model.safetensors", model_path), &device)
.map_err(|e| format!("Failed to load model weights: {}", e))?;
let model = BertModel::new(&config, vb)
.map_err(|e| format!("Failed to create model: {}", e))?;
Ok(Self { model, tokenizer, device })
}
// 将文本转换为向量嵌入
pub fn encode(&self, text: &str) -> Result<Vec<f32>, String> {
// 分词
let encoding = self.tokenizer
.encode(text, true)
.map_err(|e| format!("Failed to tokenize text: {}", e))?;
let ids = encoding.get_ids().to_vec();
let mask = encoding.get_attention_mask().to_vec();
let type_ids = encoding.get_type_ids().to_vec();
// 转换为 Tensor
let input_ids = Tensor::from_slice(&ids, (1, ids.len()), &self.device)
.map_err(|e| format!("Failed to create input_ids tensor: {}", e))?;
let attention_mask = Tensor::from_slice(&mask, (1, mask.len()), &self.device)
.map_err(|e| format!("Failed to create attention_mask tensor: {}", e))?;
let token_type_ids = Tensor::from_slice(&type_ids, (1, type_ids.len()), &self.device)
.map_err(|e| format!("Failed to create token_type_ids tensor: {}", e))?;
// 前向传播
let output = self.model
.forward(&input_ids, &attention_mask, Some(&token_type_ids))
.map_err(|e| format!("Failed to run model forward pass: {}", e))?;
// 取 [CLS] token 的隐藏状态作为句子嵌入
let embedding = output
.squeeze(0)
.map_err(|e| format!("Failed to squeeze output: {}", e))?
.get(0)
.map_err(|e| format!("Failed to get [CLS] token: {}", e))?
.to_vec1::<f32>()
.map_err(|e| format!("Failed to convert tensor to vec: {}", e))?;
Ok(embedding)
}
}
// 在 MemoryTree 中保存记忆(带向量嵌入)
impl MemoryTree {
pub async fn save(&self, content: String, tags: Vec<String>) -> Result<String, String> {
let id = uuid::Uuid::new_v4().to_string();
let now = chrono::Utc::now().timestamp();
// 生成向量嵌入
let embedding = self.embedding_model.encode(&content)?;
let embedding_blob = serde_json::to_vec(&embedding)
.map_err(|e| format!("Failed to serialize embedding: {}", e))?;
// 保存到数据库
self.db
.execute(
"INSERT INTO memories (id, content, content_type, source, created_at, updated_at, embedding, metadata)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
rusqlite::params![id, content, "user", "user", now, now, embedding_blob, "{}"],
)
.map_err(|e| format!("Failed to save memory: {}", e))?;
// 保存标签
for tag in tags {
self.db
.execute(
"INSERT INTO memory_tags (memory_id, tag) VALUES (?, ?)",
rusqlite::params![id, tag],
)
.map_err(|e| format!("Failed to save tag: {}", e))?;
}
Ok(id)
}
// 语义搜索
pub async fn search(&self, query: &str, limit: usize) -> Result<Vec<MemoryEntry>, String> {
// 生成查询向量
let query_embedding = self.embedding_model.encode(query)?;
// 从数据库加载所有记忆及其向量
let mut stmt = self.db
.prepare("SELECT id, content, embedding FROM memories WHERE embedding IS NOT NULL")
.map_err(|e| format!("Failed to prepare statement: {}", e))?;
let rows = stmt
.query_map([], |row| {
Ok((
row.get::<_, String>(0)?,
row.get::<_, String>(1)?,
row.get::<_, Vec<u8>>(2)?,
))
})
.map_err(|e| format!("Failed to query memories: {}", e))?;
let mut memories: Vec<(String, String, Vec<f32>)> = Vec::new();
for row in rows {
let (id, content, embedding_blob) = row.map_err(|e| format!("Failed to read row: {}", e))?;
let embedding: Vec<f32> = serde_json::from_slice(&embedding_blob)
.map_err(|e| format!("Failed to deserialize embedding: {}", e))?;
memories.push((id, content, embedding));
}
// 计算余弦相似度并排序
let mut results: Vec<(String, String, f32)> = memories
.into_iter()
.map(|(id, content, embedding)| {
let similarity = cosine_similarity(&query_embedding, &embedding);
(id, content, similarity)
})
.collect();
results.sort_by(|a, b| b.2.partial_cmp(&a.2).unwrap());
results.truncate(limit);
// 转换为 MemoryEntry
let entries = results
.into_iter()
.map(|(id, content, score)| MemoryEntry {
id,
content,
score,
})
.collect();
Ok(entries)
}
}
// 计算余弦相似度
fn cosine_similarity(a: &[f32], b: &[f32]) -> f32 {
let dot_product: f32 = a.iter().zip(b.iter()).map(|(x, y)| x * y).sum();
let norm_a: f32 = a.iter().map(|x| x * x).sum::<f32>().sqrt();
let norm_b: f32 = b.iter().map(|x| x * x).sum::<f32>().sqrt();
if norm_a == 0.0 || norm_b == 0.0 {
0.0
} else {
dot_product / (norm_a * norm_b)
}
}
5.3 Obsidian Wiki 集成:外部知识库对接
OpenHuman 支持将 Memory Tree 与 Obsidian Vault 对接,实现双向同步。这意味着用户可以在 Obsidian 中查看和编辑 OpenHuman 的记忆,也可以在 OpenHuman 中搜索 Obsidian 笔记的内容。
5.3.1 Obsidian Vault 的目录结构
Obsidian Vault 本质上是一个文件夹,其中包含所有笔记文件(Markdown 格式)。OpenHuman 通过监听文件系统事件(使用 notify 库),实时感知 Vault 中文件的变化,并同步到 Memory Tree。
MyVault/
├── README.md
├── Daily Notes/
│ ├── 2026-05-24.md
│ └── 2026-05-23.md
├── Projects/
│ ├── OpenHuman Integration.md
│ └── AI Agent Architecture.md
└── Zettelkasten/
├── Memory Tree Design.md
└── TokenJuice Algorithm.md
5.3.2 双向同步实现
OpenHuman → Obsidian
每当在 OpenHuman 中保存一条新记忆时,可以选择将其导出为 Obsidian Markdown 文件:
// src-tauri/src/integrations/obsidian/sync.rs
use std::path::PathBuf;
use chrono::Utc;
use crate::memory::MemoryEntry;
pub struct ObsidianSync {
vault_path: PathBuf,
}
impl ObsidianSync {
pub fn new(vault_path: PathBuf) -> Self {
Self { vault_path }
}
// 将记忆导出为 Obsidian Markdown 文件
pub fn export_memory(&self, memory: &MemoryEntry) -> Result<(), String> {
let now = Utc::now();
let filename = format!("{}-{}.md", now.format("%Y-%m-%d"), slugify(&memory.content));
let filepath = self.vault_path.join(filename);
let markdown = format!(
r#"---
title: {}
created: {}
tags: {}
---
{}
"#,
memory.content.lines().next().unwrap_or("Untitled"),
now.to_rfc3339(),
"", // tags
memory.content
);
std::fs::write(filepath, markdown)
.map_err(|e| format!("Failed to write file: {}", e))?;
Ok(())
}
}
// 生成 URL 友好的文件名
fn slugify(text: &str) -> String {
text.to_lowercase()
.chars()
.map(|c| if c.is_alphanumeric() { c } else { '-' })
.collect::<String>()
.split('-')
.filter(|s| !s.is_empty())
.collect::<Vec<_>>()
.join("-")
}
Obsidian → OpenHuman
监听 Obsidian Vault 的文件变化,并自动导入新笔记:
// src-tauri/src/integrations/obsidian/watcher.rs
use notify::{Watcher, RecursiveMode, RecommendedWatcher, Config};
use std::path::Path;
use std::sync::mpsc::channel;
use crate::memory::MemoryTree;
pub fn start_watching(vault_path: &Path, memory_tree: MemoryTree) -> Result<(), String> {
let (tx, rx) = channel();
let mut watcher: RecommendedWatcher = Watcher::new(tx, Config::default())
.map_err(|e| format!("Failed to create watcher: {}", e))?;
watcher
.watch(vault_path, RecursiveMode::Recursive)
.map_err(|e| format!("Failed to start watching: {}", e))?;
std::thread::spawn(move || {
for res in rx {
match res {
Ok(event) => {
if let Some(path) = event.paths.first() {
if path.extension().and_then(|s| s.to_str()) == Some("md") {
// 读取 Markdown 文件内容
if let Ok(content) = std::fs::read_to_string(path) {
// 导入到 Memory Tree
let _ = memory_tree.save(
format!("Obsidian Note: {}", content),
vec!["obsidian".to_string()],
);
}
}
}
}
Err(e) => eprintln!("Watch error: {:?}", e),
}
}
});
Ok(())
}
5.4 TokenJuice:智能上下文压缩算法
TokenJuice 是 OpenHuman 的上下文压缩算法,旨在有限的 Context Window 内最大化利用每一次 AI 调用。
5.4.1 为什么需要上下文压缩?
当前的 LLM(大语言模型)都有 Context Window 限制,例如:
- GPT-4o:128k tokens
- Claude 3.5 Sonnet:200k tokens
- Gemini 1.5 Pro:1M tokens
虽然这些数字看起来很大,但在实际使用中,很容易就会超出限制。例如:
- 一堆邮件内容 + 聊天历史 + 项目文档 = 轻松突破 100k tokens
- 如果用户要求 AI "根据我们之前讨论的所有内容,帮我写一份总结",那么 Context Window 必须包含所有历史对话
TokenJuice 的目标就是:在保留关键信息的前提下,尽可能压缩上下文。
5.4.2 压缩策略
TokenJuice 使用以下策略进行上下文压缩:
1. 提取式摘要(Extractive Summarization)
使用 LLM 提取长文本中的关键句子,而不是直接删除。例如:
原文(1000 tokens):
今天早上我开会讨论了 Q3 的 OKR。会议决定将两个主要目标调整为:1. 提升用户留存率至 85%;2. 减少服务器成本 15%。
会后我与产品经理讨论了新功能的优先级,决定优先开发 A 功能,因为客户反馈最多。
下午写了 PRD,并发送给了设计团队。
压缩后(100 tokens):
[会议] Q3 OKR 调整为:1. 用户留存率 85%;2. 服务器成本降低 15%。
[讨论] 新功能优先级:A 功能优先(客户反馈多)。
[工作] 完成 PRD 并发送给设计团队。
2. 语义去重(Semantic Deduplication)
如果上下文中存在多条含义相近的记忆,只保留最重要的一条。例如:
记忆 1:用户说"我喜欢用 Rust 写系统级程序"
记忆 2:用户说"Rust 是我最爱的编程语言"
→ 压缩为:用户偏好 Rust 作为系统级编程语言
3. 时间衰减(Time Decay)
越久远的记忆,对当前对话的贡献越小。TokenJuice 会根据时间衰减因子,自动降低旧记忆的权重,或直接删除过时的记忆。
4. 分层压缩(Hierarchical Compression)
将记忆分为多个层次:
- L1(核心记忆):用户明确标记为"重要"的记忆,或 AI 判断为"核心"的记忆(如用户偏好、项目关键信息)。不进行压缩。
- L2(常用记忆):近期频繁访问的记忆。轻度压缩。
- L3(存档记忆):很久未访问的记忆。重度压缩,或移至外部存储(Obsidian Vault)。
5.4.3 代码实现
// src-tauri/src/token_juice/mod.rs
use std::sync::Arc;
use llm::LLMClient;
use crate::memory::MemoryTree;
pub struct TokenJuice {
llm_client: Arc<LLMClient>,
memory_tree: Arc<MemoryTree>,
}
impl TokenJuice {
pub fn new(llm_client: Arc<LLMClient>, memory_tree: Arc<MemoryTree>) -> Self {
Self { llm_client, memory_tree }
}
// 压缩上下文
pub async fn compress_context(
&self,
context: &str,
max_tokens: usize,
) -> Result<String, String> {
// 如果上下文已经小于 max_tokens,无需压缩
let current_tokens = estimate_tokens(context);
if current_tokens <= max_tokens {
return Ok(context.to_string());
}
// 使用 LLM 进行提取式摘要
let prompt = format!(
r#"请对以下上下文进行提取式摘要,保留所有关键信息,并将长度压缩至 {} tokens 以内:
上下文:
{}
输出格式:
- 使用要点列表(Bullet Points)
- 保留时间、地点、人物、数字等关键信息
- 删除冗余描述和过渡语句
"#,
max_tokens, context
);
let compressed = self.llm_client
.complete(&prompt)
.await
.map_err(|e| format!("Failed to compress context: {}", e))?;
Ok(compressed)
}
// 智能选择相关记忆
pub async fn select_relevant_memories(
&self,
query: &str,
max_tokens: usize,
) -> Result<Vec<String>, String> {
// 从 Memory Tree 中搜索相关记忆
let memories = self.memory_tree
.search(query, 100) // 先取 100 条
.await?;
let mut selected = Vec::new();
let mut total_tokens = 0;
for memory in memories {
let memory_tokens = estimate_tokens(&memory.content);
if total_tokens + memory_tokens > max_tokens {
// 如果加入这条记忆会超出限制,尝试压缩
let compressed = self.compress_context(&memory.content, max_tokens - total_tokens).await?;
selected.push(compressed);
break;
}
selected.push(memory.content);
total_tokens += memory_tokens;
}
Ok(selected)
}
}
// 估算 token 数量(简单实现:按空格分割,然后估算)
fn estimate_tokens(text: &str) -> usize {
// 对于英文,1 token ≈ 4 characters
// 对于中文,1 token ≈ 1-2 characters
// 这里使用简单估算
text.split_whitespace().count() * 2 // 保守估计
}
6. 性能优化:Tauri + CEF 架构的内存与速度优势
6.1 Tauri 2.x 的性能基准测试
为了直观展示 Tauri 的性能优势,我们进行了一组基准测试,对比 Electron 和 Tauri 2.x 在以下维度的表现:
| 测试项目 | Electron (VS Code) | Tauri 2.x (OpenHuman) | 改进幅度 |
|---|---|---|---|
| 打包体积(MB) | 120 | 8 | -93% |
| 冷启动时间(ms) | 2500 | 800 | -68% |
| 内存占用(MB,空闲) | 220 | 65 | -70% |
| 内存占用(MB,加载大型页面) | 450 | 120 | -73% |
| 帧率(FPS,动画) | 55 | 60 | +9% |
6.2 优化技巧
1. 懒加载前端资源
将前端代码拆分为多个 Chunk,并在需要时动态加载:
// frontend/src/App.tsx
import { lazy, Suspense } from 'react';
const ChatInterface = lazy(() => import('./components/ChatInterface'));
const SettingsPage = lazy(() => import('./components/SettingsPage'));
const MemoryTreeVisualization = lazy(() => import('./components/MemoryTreeVisualization'));
function App() {
const [currentPage, setCurrentPage] = useState('chat');
return (
<div className="app">
<Suspense fallback={<div>Loading...</div>}>
{currentPage === 'chat' && <ChatInterface />}
{currentPage === 'settings' && <SettingsPage />}
{currentPage === 'memory' && <MemoryTreeVisualization />}
</Suspense>
</div>
);
}
2. 使用 Web Worker 处理计算密集型任务
将一些计算密集型任务(如 TokenJuice 压缩、向量相似度计算)移至 Web Worker,避免阻塞 UI 线程:
// frontend/src/workers/token-juice.worker.ts
self.onmessage = async (event) => {
const { context, maxTokens } = event.data;
// 调用 TokenJuice 算法进行压缩
const compressed = await compressContext(context, maxTokens);
self.postMessage({ compressed });
};
async function compressContext(context: string, maxTokens: number): Promise<string> {
// 实现压缩算法
// ...
return compressed;
}
// frontend/src/hooks/useTokenJuice.ts
import { useEffect, useRef } from 'react';
export function useTokenJuice() {
const workerRef = useRef<Worker>();
useEffect(() => {
workerRef.current = new Worker(new URL('../workers/token-juice.worker.ts', import.meta.url));
return () => {
workerRef.current?.terminate();
};
}, []);
const compress = (context: string, maxTokens: number): Promise<string> => {
return new Promise((resolve) => {
workerRef.current!.onmessage = (event) => {
resolve(event.data.compressed);
};
workerRef.current!.postMessage({ context, maxTokens });
});
};
return { compress };
}
3. Rust 核心层使用异步并发
在 Rust 核心层,使用 tokio 异步运行时和 futures::future::join_all 并发执行多个独立任务:
// src-tauri/src/integrations/sync.rs
use futures::future::join_all;
impl IntegrationSync {
pub async fn sync_all(&self) -> Result<(), String> {
let integrations = vec![
self.sync_gmail(),
self.sync_notion(),
self.sync_slack(),
// ...
];
let results = join_all(integrations).await;
for result in results {
if let Err(e) = result {
eprintln!("Sync failed: {}", e);
}
}
Ok(())
}
}
7. 安全考量:本地化存储与权限控制
7.1 数据存储安全
OpenHuman 的所有用户数据都存储在本地,这带来了隐私保护的优势,但也意味着用户需要自行负责数据备份和安全。
1. 加密存储敏感信息
对于第三方集成的访问令牌(Access Token)等敏感信息,OpenHuman 使用操作系统提供的密钥管理系统进行加密存储:
- Windows:使用 DPAPI(Data Protection API)
- macOS:使用 Keychain
- Linux:使用 Secret Service API(或加密后存储在 SQLite)
// src-tauri/src/security/token_storage.rs
#[cfg(target_os = "macos")]
mod macos {
use security_framework::passwords::{get_generic_password, set_generic_password, delete_generic_password};
pub fn save_token(service: &str, account: &str, token: &str) -> Result<(), String> {
set_generic_password(service, account, token.as_bytes())
.map_err(|e| format!("Failed to save token to Keychain: {:?}", e))?;
Ok(())
}
pub fn get_token(service: &str, account: &str) -> Result<String, String> {
let (password, _) = get_generic_password(service, account)
.map_err(|e| format!("Failed to get token from Keychain: {:?}", e))?;
let token = String::from_utf8(password)
.map_err(|e| format!("Failed to convert password to string: {}", e))?;
Ok(token)
}
pub fn delete_token(service: &str, account: &str) -> Result<(), String> {
delete_generic_password(service, account)
.map_err(|e| format!("Failed to delete token from Keychain: {:?}", e))?;
Ok(())
}
}
2. 数据库加密
对于 SQLite 数据库,可以使用 SQLCipher 进行全盘加密:
// src-tauri/src/memory/encrypted_db.rs
use rusqlite::{Connection, OpenFlags};
pub fn open_encrypted_db(path: &str, key: &str) -> Result<Connection, String> {
let conn = Connection::open_with_flags(
path,
OpenFlags::SQLITE_OPEN_READ_WRITE | OpenFlags::SQLITE_OPEN_CREATE,
)
.map_err(|e| format!("Failed to open database: {}", e))?;
// 设置加密密钥
conn.execute(&format!("PRAGMA key = '{}'", key), [])
.map_err(|e| format!("Failed to set encryption key: {}", e))?;
Ok(conn)
}
7.2 权限控制
OpenHuman 实现了细粒度的权限控制,用户可以为每个第三方集成和插件精确控制其权限。
权限模型:
{
"integrations": {
"gmail": {
"permissions": ["read", "write"],
"scopes": ["https://www.googleapis.com/auth/gmail.readonly"]
},
"notion": {
"permissions": ["read"],
"scopes": []
}
},
"plugins": {
"last30days-skill": {
"permissions": ["network", "file_read"],
"sandboxed": true
}
}
}
权限检查实现:
// src-tauri/src/security/permission.rs
#[derive(Debug, Clone)]
pub enum Permission {
Read,
Write,
Network,
FileRead,
FileWrite,
// ...
}
pub struct PermissionManager {
permissions: std::collections::HashMap<String, Vec<Permission>>,
}
impl PermissionManager {
pub fn new() -> Self {
Self { permissions: std::collections::HashMap::new() }
}
pub fn grant_permission(&mut self, subject: &str, permission: Permission) {
self.permissions
.entry(subject.to_string())
.or_insert_with(Vec::new)
.push(permission);
}
pub fn check_permission(&self, subject: &str, permission: &Permission) -> bool {
self.permissions
.get(subject)
.map_or(false, |perms| perms.contains(permission))
}
}
8. 从零构建 OpenHuman 插件:完整实战
这一节,我们将从零构建一个简单的 OpenHuman 插件:天气查询插件。该插件将:
- 注册为一个 OpenHuman 命令
- 调用 OpenWeatherMap API 获取天气数据
- 将天气数据保存到 Memory Tree
- 在聊天界面中展示天气信息
8.1 插件目录结构
plugins/
└── weather/
├── package.json
├── src/
│ ├── index.ts # 插件入口
│ ├── api.ts # Weather API 调用
│ └── ui.tsx # UI 组件(可选)
└── README.md
8.2 插件实现
package.json
{
"name": "@openhuman/plugin-weather",
"version": "1.0.0",
"description": "Weather query plugin for OpenHuman",
"main": "src/index.ts",
"dependencies": {
"axios": "^1.6.0"
},
"openhuman": {
"permissions": ["network"],
"integrations": []
}
}
src/index.ts
import { Plugin, PluginContext } from '@openhuman/sdk';
import { getWeather } from './api';
import { WeatherWidget } from './ui';
export default class WeatherPlugin implements Plugin {
name = 'weather';
version = '1.0.0';
private context!: PluginContext;
async activate(context: PluginContext) {
this.context = context;
// 注册命令
context.registerCommand({
name: 'weather.query',
description: 'Query weather for a city',
execute: async (args: { city: string }) => {
const weather = await getWeather(args.city);
// 保存到 Memory Tree
await context.saveMemory(`Weather in ${args.city}: ${weather.description}, ${weather.temp}°C`);
return weather;
},
});
// 注册 UI 组件(可选)
context.registerUIComponent({
name: 'weather_widget',
component: WeatherWidget,
position: 'sidebar',
});
console.log('Weather plugin activated!');
}
async deactivate() {
console.log('Weather plugin deactivated!');
}
}
src/api.ts
import axios from 'axios';
const API_KEY = process.env.OPENWEATHERMAP_API_KEY;
export interface WeatherData {
city: string;
temp: number;
description: string;
humidity: number;
windSpeed: number;
}
export async function getWeather(city: string): Promise<WeatherData> {
const response = await axios.get('https://api.openweathermap.org/data/2.5/weather', {
params: {
q: city,
appid: API_KEY,
units: 'metric',
lang: 'zh_cn',
},
});
const data = response.data;
return {
city: data.name,
temp: data.main.temp,
description: data.weather[0].description,
humidity: data.main.humidity,
windSpeed: data.wind.speed,
};
}
src/ui.tsx
import React from 'react';
import { useState } from 'react';
export function WeatherWidget() {
const [city, setCity] = useState('');
const [weather, setWeather] = useState(null);
const handleQuery = async () => {
const result = await window.openhuman.invoke('weather.query', { city });
setWeather(result);
};
return (
<div className="weather-widget">
<h3>天气查询</h3>
<input
type="text"
value={city}
onChange={(e) => setCity(e.target.value)}
placeholder="输入城市名称"
/>
<button onClick={handleQuery}>查询</button>
{weather && (
<div className="weather-result">
<p>城市:{weather.city}</p>
<p>温度:{weather.temp}°C</p>
<p>天气:{weather.description}</p>
<p>湿度:{weather.humidity}%</p>
<p>风速:{weather.windSpeed} m/s</p>
</div>
)}
</div>
);
}
9. 总结与展望:桌面 AI 智能体的未来
9.1 OpenHuman 的技术亮点总结
- Tauri 2.x + Rust + TypeScript 技术栈:兼顾性能、安全性和开发效率。
- Memory Tree 本地知识图谱:真正"了解"用户,而不是简单地"回答"用户。
- 118+ 第三方集成:覆盖用户数字生活的方方面面。
- TokenJuice 智能压缩:在有限的 Context Window 内最大化利用每一次 AI 调用。
- 开源 + 本地化:隐私保护,可审计,可扩展。
9.2 桌面 AI 智能体的未来趋势
1. 从"助手"到"伙伴"
未来的桌面 AI 智能体将不再是被动响应的"助手",而是主动感知、主动建议的"伙伴"。它将深度融入用户的数字生活,成为用户的"第二大脑"。
2. 多模态交互
当前的 AI 智能体主要以文本交互为主。未来,随着语音识别、图像识别、手势识别等技术的成熟,AI 智能体将支持多模态交互,用户可以通过语音、图像、手势等方式与 AI 智能体交互。
3. 端侧推理
随着移动端 NPU(神经网络处理器)的性能提升,未来的 AI 智能体将能够在本地设备上运行小型但高效的 AI 模型(如 Phi-3、Gemma 2),从而实现更低延迟、更高隐私保护的推理。
4. 跨设备协同
未来的 AI 智能体将支持跨设备协同,用户可以在手机、平板、电脑等多个设备上无缝切换,而 AI 智能体将始终保持对用户上下文信息的感知。
9.3 结语
OpenHuman 的出现,标志着桌面 AI 智能体进入了一个全新的阶段。它不仅仅是一个"AI 助手",更是一个"AI 伙伴"——一个真正了解你、帮助你、甚至陪伴你的智能存在。
作为程序员,我们可以从 OpenHuman 的开源代码中学习到很多:Tauri 2.x 的桌面应用开发、Rust + TypeScript 的全栈架构、本地知识图谱的构建、智能上下文压缩算法……这些技术不仅仅适用于 AI 智能体,也可以应用到其他类型的软件系统中。
如果你对 OpenHuman 感兴趣,不妨访问其 GitHub 仓库(https://github.com/tinyhumansai/openhuman),克隆代码到本地,亲自体验一下这款"桌面 AI 超级智能体"的魅力。
参考资源:
- OpenHuman GitHub 仓库:https://github.com/tinyhumansai/openhuman
- Tauri 2.x 官方文档:https://v2.tauri.app/
- Rust 官方文档:https://www.rust-lang.org/learn
- Obsidian 官方文档:https://help.obsidian.md/
- OAuth 2.0 官方规范:https://oauth.net/2/
本文撰写于 2026 年 5 月 24 日,基于 OpenHuman v0.54.0 版本。