diff --git a/gitbooks/developing/architecture.zh-CN.md b/gitbooks/developing/architecture.zh-CN.md new file mode 100644 index 0000000000..0e9cf7f774 --- /dev/null +++ b/gitbooks/developing/architecture.zh-CN.md @@ -0,0 +1,353 @@ +--- +description: OpenHuman 代码库的深度架构参考 —— 仓库布局、运行时范围、双 socket 同步、RPC 流程。 +icon: code-branch +lang: zh-CN +--- + +# OpenHuman 架构 + +**基于 Rust 构建的加密社区 AI 超级助手。** + +OpenHuman 是一款为加密货币生态系统量身打造的跨平台通信与自动化平台。单一的 React + Rust(Tauri)代码库可以面向多个平台;**我们目前为用户文档和发布的仅是桌面端** —— **Windows、macOS 和 Linux**。Android、iOS 和 Web **尚未**在当前文档或发布中支持。技术栈包括一个托管的 Node.js 运行时,用于支持工具能力的技能;持久化的 Rust 原生 WebSocket 基础设施;以及一个 AI 工具协议,让语言模型实时调用任何已连接的服务。 + +--- + +## 仓库布局(monorepo) + +| 路径 | 内容 | +| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| **`app/`** | Yarn workspace **`openhuman-app`**:Vite/React UI(`app/src/`)、Tauri 壳层(`app/src-tauri/`)、Vitest 测试 | +| **仓库根目录 `src/`** | Rust **`openhuman_core`** 库 + **`openhuman-core`** CLI 二进制文件 —— 核心服务器、JSON-RPC、一等 JavaScript 运行时(`src/openhuman/javascript/`),由托管的 Node.js 实现驱动、频道、内存等 | +| **`Cargo.toml`**(根目录) | 构建 `openhuman-core` 二进制文件(`cargo build --bin openhuman-core`),staging 到 `app/src-tauri/binaries/` 以供桌面打包 | +| **`skills/`** | 运行时消耗的技能包 | +| **`docs/`** | 本书 + 每棵树指南(`docs/src/`、`docs/src-tauri/`) | + +桌面应用 **WebView** 从 `app/` 加载 UI;繁重的 RPC 和技能在 **`openhuman-core`** 进程中运行,可通过 HTTP 从 Tauri 主机访问(`core_rpc_relay`)。 + +--- + +## 平台覆盖范围 + +**今天支持的(终端用户):** 桌面端。Windows、macOS、Linux(原生安装包)。 + +**尚未支持:** Android、iOS、独立 Web 客户端(仓库中可能以实验性目标存在;不要视为产品就绪)。 + +```text + OpenHuman(已发布) + | + Desktop + / | \ + Windows macOS Linux + x64 x64 x64 + ARM64 ARM64 ARM64 +``` + +Tauri v2 将 Rust 核心编译为每个平台的原生二进制文件,将 React 前端作为轻量级 WebView 嵌入。桌面构建产出 `.dmg`、`.msi`、`.AppImage` 和 `.deb` 安装包。额外目标(移动端、Web)在明确文档化支持之前均超出范围。 + +--- + +## 高层架构 + +```text ++------------------------------------------------------------------+ +| React 前端 | +| Redux Toolkit | Socket.io 客户端 | MCP 传输层 | UI | ++------------------------------------------------------------------+ + | Tauri IPC 桥接 | ++------------------------------------------------------------------+ +| Rust 核心引擎 | +| | +| +------------------+ +------------------+ +-----------------+ | +| | QuickJS 技能 | | Socket 管理器 | | AI 加密 | | +| | 运行时引擎 | | (持久化 WS) | | & 内存存储 | | +| +------------------+ +------------------+ +-----------------+ | +| | +| +------------------+ +------------------+ +-----------------+ | +| | 技能注册表 | | Cron 调度器 | | 会话 & 认证 | | +| | & 桥接 API | | (5s tick 循环) | | 管理 | | +| +------------------+ +------------------+ +-----------------+ | +| | +| +------------------+ +------------------+ +-----------------+ | +| | Telegram | | SQLite 存储 | | OS 钥匙串 | | +| | 集成 | | (rusqlite) | | 集成 | | +| +------------------+ +------------------+ +-----------------+ | ++------------------------------------------------------------------+ + | + +-----------+-----------+ + | | + 后端服务 外部 API + (Socket.io 服务器) (Telegram 等) +``` + +前端通过两种方式与 **openhuman** Rust 核心通信:用于一小部分壳层命令的 **Tauri IPC**(窗口、AI 文件辅助函数、**`core_rpc_relay`**),以及用于业务逻辑和技能的 **HTTP JSON-RPC**。核心拥有持久连接(如适用)、内存/功能的加密工作,以及 **QuickJS** 沙盒化技能执行。 + +--- + +## Rust 驱动的性能 + +OpenHuman 选择 Tauri + Rust 而非 Electron,基于根本的性能和安全原因: + +| 指标 | OpenHuman(Tauri + Rust) | 典型 Electron 应用 | +| ------------------------- | -------------------------------------------------------- | ---------------------------- | +| 二进制体积 | 取决于功能(CEF 运行时 + 技能包占主导) | ~150 MB+ | +| 每技能上下文内存 | ~1-2 MB(QuickJS) | ~150 MB+(Chromium 渲染器) | +| 冷启动 | 亚 500ms | 2-5 秒 | +| 垃圾回收暂停 | 无(Rust 所有权模型) | V8 GC 暂停 | +| 内存安全 | 编译期保证 | 运行时异常 | +| TLS 实现 | rustls(无 OpenSSL 依赖) | Chromium 的 BoringSSL | + +**这对加密平台为何重要**:交易员和分析师在运行 OpenHuman 的同时,还会运行资源密集型工具、图表软件、多个浏览器标签、交易终端。原生二进制文件加上亚 500ms 启动意味着应用感觉像原生应用,不会碍事。零 GC 暂停意味着实时价格推送和警报永远不会因内存管理而延迟。 + +**Tokio 异步运行时**驱动所有 I/O。WebSocket 连接、HTTP 请求、文件操作和技能间通信,都是线程池上的非阻塞任务。数千个并发操作(技能执行、cron job、socket 事件)共享一小套固定的 OS 线程。 + +--- + +## 实时 Socket 基础设施 + +OpenHuman 实现了**双 socket 架构**:桌面端使用 Rust 原生 WebSocket 客户端,Web 端使用 JavaScript Socket.io 客户端。Rust 实现能在应用后台存活,独立于 WebView 运行,并通过 rustls 处理 TLS。 + +```text +桌面模式: Web 模式: + ++-------------+ +-------------+ +| React UI | | React UI | ++------+------+ +------+------+ + | Tauri IPC | Direct ++------+------+ +------+------+ +| Rust Socket | | JS Socket | +| Manager | | .io Client | ++------+------+ +------+------+ + | tokio-tungstenite | Socket.io + | + rustls TLS | (websocket/polling) ++------+------+ +------+------+ +| Backend | | Backend | ++-------------+ +-------------+ +``` + +**Rust Socket 管理器**通过原始 WebSocket 实现 Engine.IO v4 + Socket.IO v4 帧: + +- **握手**:WebSocket 连接、Engine.IO OPEN(提取 `sid`、`pingInterval`、`pingTimeout`)、带 JWT 认证的 Socket.IO CONNECT、CONNECT ACK +- **保活**:响应 Engine.IO PING 以 PONG;超时阈值 = `pingInterval + pingTimeout + 5s`(默认:50 秒) +- **重连**:指数退避,从 1 秒到最大 30 秒。成功连接丢失后重置为 1s;如果连接从未建立则持续增长 +- **CORS 绕过**:Rust `reqwest` HTTP 客户端直接发起外部 API 调用,不受浏览器 CORS 限制 + +socket 连接在所有技能间**共享**。当事件到达时,socket 管理器通过异步消息通道将它们路由到相应的技能。这完全消除了每个技能的连接开销。 + +**`tool:sync` 协议**:每次 socket 连接和技能生命周期变化时,客户端都会发出一个 `tool:sync` 事件,包含可用工具的完整列表及其连接状态。这使后端 AI 系统能实时感知所有能力。 + +--- + +## 技能运行时引擎 + +OpenHuman 的决定性能力是其运行在 Rust 进程内部的**沙盒化 JavaScript 执行引擎**。技能是轻量级自动化脚本,通过自定义工具、集成和定时任务扩展平台。 + +```text ++---------------------------------------------------------------+ +| RuntimeEngine | +| | +| +-------------------+ +-------------------+ | +| | SkillRegistry | | CronScheduler | | +| | (HashMap + MPSC) | | (5s tick loop) | | +| +--------+----------+ +--------+----------+ | +| | | | +| +--------v----------+ +--------v----------+ +----------+ | +| | JavaScript Layer | | runtime_node | | Bridge | | +| | skill metadata | | managed Node.js | | APIs | | +| | + prompt context | | system/bundled | +----+-----+ | +| | + tool discovery | | tool execution | | | +| +-------------------+ +-------------------+ | | +| | | +| +---------------------------------------------------v-----+ | +| | net | db | store | cron | log | tauri | | | +| | HTTP SQLite KV Schedule Log Platform| | | +| +------------------------------------------------------+ | | ++---------------------------------------------------------------+ +``` + +**Node.js 运行时**:核心尽可能解析兼容的系统 `node`,否则将托管发行版安装到 OpenHuman 缓存中。技能主要暴露工具元数据,并使用运行时桥接来列出和执行工具,而非在核心内运行隔离的 QuickJS VM。 + +| 参数 | 值 | +| ---------------------- | ----- | +| 公共语言槽位 | `javascript` | +| 当前 JS 后端 | `runtime_node` | +| 托管 Node 版本 | 默认 `v22.11.0` | +| 运行时来源 | 系统 `node` 或托管安装 | +| 完整性验证 | 针对 `SHASUMS256.txt` 的 SHA-256 | + +**工具桥架构**:`SKILL.md` 包提供元数据、指令和可选的捆绑 JS 辅助函数。Rust 核心拥有权威的工具注册表,JavaScript 运行时桥接列出工具并将具名工具调用分派到核心或 Node-backed 辅助函数中。 + +**桥接 API** 向运行时桥接和 Node-backed 辅助函数暴露平台能力: + +| 桥接 | 能力 | +| --------- | ----------------------------------------------------------- | +| **net** | 通过 `reqwest` 的 HTTP fetch(默认 30s 超时,所有方法) | +| **db** | 通过 `rusqlite` 的每个技能 SQLite 数据库 | +| **store** | 键值持久化 | +| **cron** | 定时注册(6 字段 cron 表达式) | +| **log** | 通过 Rust `log` crate 的结构化日志 | +| **tauri** | 平台检测、通知、白名单环境变量 | + +**技能发现** 使用 `SKILL.md` 加上可选的捆绑资源: + +| 字段 | 用途 | +| ------------------ | ------- | +| `name` | 人类可读的显示名称 | +| `description` | 触发/选择摘要 | +| `metadata.id` | 存在时的稳定技能 slug | +| `allowed-tools` | 工具允许列表指引 | +| 捆绑资源 | 脚本、参考、资源 | + +技能从 GitHub 仓库同步并在运行时发现。执行不再建模为每个技能一个嵌入式 QuickJS VM;JavaScript 行为通过共享运行时桥接流动。 + +**Cron 调度器**:一个 5 秒 tick 循环对照 UTC 时间检查所有已注册的调度,使用 `cron` crate 进行表达式解析。当调度触发时,调度器向技能的通道发送 `CronTrigger` 消息,调用技能的 `onCronTrigger()` 处理程序。 + +--- + +## AI & 工具协议(MCP) + +OpenHuman 实现了**模型上下文协议**,一个基于 Socket.io 的 JSON-RPC 2.0 层,让 AI 模型发现并由技能暴露的工具。 + +```text +用户提示 + | + v +AI 模型(后端) + | + | 1. mcp:listTools --> 前端/Rust 聚合所有技能工具 + | <-- 工具目录 + | + | 2. 决定调用哪个工具 + | + | 3. mcp:toolCall { skillId__toolName, arguments } + | | + | v + | Socket 管理器路由到技能注册表 + | | + | v + | QuickJS 技能实例执行工具 + | | + | v + | 桥接 API 调用(HTTP、DB 等) + | | + | <-- mcp:toolCallResponse { result } + | + v +AI 对用户的响应 +``` + +**传输**:每次请求 30 秒超时,`mcp:` 事件前缀,请求 ID 在待处理响应映射中跟踪。工具名称以 `skillId__toolName` 命名空间化,以实现明确路由。 + +**工具同步**:`tool:sync` 事件在每次 socket 连接和技能状态变化时广播完整的工具清单、技能 ID、名称、连接状态和工具列表。后端 AI 系统始终拥有可用能力的最新视图。 + +**AI 记忆系统**: + +| 功能 | 实现 | +| ------------------ | ------------------------------------------------------ | +| 静态加密 | 带 Argon2id 密钥派生的 AES-256-GCM | +| 分块 | 每块 512 token,64 token 重叠 | +| 搜索 | 混合:70% 向量相似度 + 30% FTS5 全文 | +| 嵌入 | OpenAI `text-embedding-3-small` | +| 知识图谱 | 通过 REST API 的 Neo4j,用于实体关系 | +| 会话 | 带压缩和工具压缩的 JSONL 转录 | + +记忆加密密钥通过 Argon2id 从用户凭证派生,确保记忆文件在未经认证的情况下不可读。混合搜索结合语义理解(向量相似度)和关键词精确度(SQLite FTS5)以实现可靠的召回。 + +--- + +## 安全架构 + +```text ++-------------------------------------------------------------------+ +| 安全层 | +| | +| +------------------+ +------------------+ +------------------+ | +| | OS 钥匙串 | | AES-256-GCM | | 沙盒化 | | +| | (macOS/Win/Lin) | | 内存加密 | | QuickJS 每 | | +| | 用于凭证 | | + Argon2id KDF | | 技能 (64 MB) | | +| +------------------+ +------------------+ +------------------+ | +| | +| +------------------+ +------------------+ +------------------+ | +| | 一次性 | | rustls TLS | | 无 localStorage | | +| | 登录 token | | 用于所有网络 | | 存储敏感数据 | | +| | (5-min TTL) | | 连接 | | | | +| +------------------+ +------------------+ +------------------+ | ++-------------------------------------------------------------------+ +``` + +- **凭证存储**:通过 `keyring` crate 的 OS 钥匙串集成(macOS Keychain、Windows Credential Manager、Linux Secret Service),仅限桌面端 +- **内存加密**:带 Argon2id 密钥派生的 AES-256-GCM。所有 AI 内存静态加密 +- **技能沙盒化**:每个 QuickJS 实例都有强制内存限制(默认 64 MB)和栈限制(512 KB)。禁止跨技能内存访问 +- **认证交接**:Web 到桌面认证使用 5 分钟 TTL 的一次性登录 token,通过 Rust HTTP 客户端交换(绕过 CORS) +- **网络 TLS**:所有 WebSocket 和 HTTP 连接使用 rustls,不依赖平台 OpenSSL +- **状态管理**:敏感数据保存在 Redux(内存)和 OS 钥匙串(持久化)中。凭证或 token 不使用 localStorage +- **提示注入防护**:用户提示在模型/工具执行前经过规范化/评分,并在服务器端强制执行(`allow | review | block`)。详见 [`docs/PROMPT_INJECTION_GUARD.md`](../../docs/PROMPT_INJECTION_GUARD.md) + +--- + +## 端到端数据流 + +从用户操作到外部服务再返回的完整流程: + +```text +用户在聊天 UI 中输入命令 + | + v +React 前端分派到 AI 提供商 + | + v +AI 模型接收提示 + 工具目录(通过 tool:sync) + | + v +AI 决定调用技能工具(例如,发送 Telegram 消息) + | + v +通过 Socket.io 发送 mcp:toolCall 事件 + | + v +Socket 管理器(Rust)接收事件,解析 skillId__toolName + | + v +技能注册表通过 MPSC 通道将消息路由到正确的 QuickJS 实例 + | + v +QuickJS 技能执行工具处理程序 + | + v +桥接 API:net.rs 通过 reqwest 发起 HTTP 请求(无 CORS,rustls TLS) + | + v +外部服务响应(例如,Telegram API) + | + v +结果回流:桥接 -> QuickJS -> 注册表 -> Socket -> MCP -> AI -> UI + | + v +用户在聊天界面中看到结果 +``` + +每一层都是异步且非阻塞的。Rust 核心在固定的 Tokio 线程池上处理数千个并发的技能执行、cron 触发和 socket 事件。 + +--- + +## 技术栈 + +| 层 | 技术 | 原因 | +| -------------- | ------------------------------- | -------------------------------------------------------- | +| **前端** | React 19, TypeScript 5.8 | 现代组件模型,类型安全 | +| **状态** | Redux Toolkit + Persist | 可预测状态,支持离线持久化 | +| **构建** | Vite 7 | 亚秒级 HMR,优化的生产构建 | +| **样式** | Tailwind CSS | 工具优先,一致的设计系统 | +| **框架** | Tauri v2 | 原生跨平台,开销最小 | +| **语言** | Rust (2021 edition) | 内存安全,零成本抽象 | +| **异步** | Tokio | 高性能异步 I/O 运行时 | +| **JS 运行时** | Node.js | 用于工具辅助函数和技能相关 JS 的托管 V8 运行时 | +| **数据库** | SQLite (rusqlite) | 嵌入式,零配置,每技能隔离 | +| **WebSocket** | tokio-tungstenite + rustls | 持久连接,原生 TLS | +| **HTTP** | reqwest | 异步 HTTP,支持 rustls + native-tLS 双栈 | +| **加密** | aes-gcm + argon2 | AES-256-GCM 加密,Argon2id 密钥派生 | +| **调度** | cron crate + 自定义调度器 | 标准 cron 表达式,5 秒精度 | +| **Telegram** | 已移除 | Telegram 集成已移除 | +| **实时** | Socket.io(客户端) | 双向基于事件的通信 | +| **AI** | MCP(JSON-RPC 2.0) | LLM 集成的标准化工具协议 | +| **搜索** | OpenAI 嵌入 + SQLite FTS5 | 混合语义 + 关键词搜索 | +| **图谱** | Neo4j | 实体关系知识图谱 | diff --git a/gitbooks/developing/architecture/README.zh-CN.md b/gitbooks/developing/architecture/README.zh-CN.md new file mode 100644 index 0000000000..7662560a9f --- /dev/null +++ b/gitbooks/developing/architecture/README.zh-CN.md @@ -0,0 +1,81 @@ +--- +description: >- + OpenHuman 系统的高层轮廓(桌面壳层、Rust 核心、Memory Tree、Agent 循环)。指向仓库中的深度开发者架构文档。 +icon: code-branch +lang: zh-CN +--- + +# 架构 + +OpenHuman 基于 GNU GPL3 开源。本页是系统的高层轮廓;深度开发者架构参考位于仓库中的 [深度架构文档](../architecture.zh-CN.md)。 + +## 系统形态 + +OpenHuman 是一款 **React + Tauri v2 桌面应用**,搭配一个承担重活的 **Rust 核心**。 + +```text +┌──────────────────────────────────────────────────┐ +│ Tauri 壳层 (app/src-tauri/) │ +│ • 窗口管理、OS 集成、sidecar 生命周期 │ +│ • 用于集成提供商的 CEF 子 WebView │ +└──────────────────────────────────────────────────┘ + │ JSON-RPC (HTTP) ↕ +┌──────────────────────────────────────────────────┐ +│ Rust 核心 (openhuman 二进制, src/) │ +│ • Memory Tree 流水线 │ +│ • 集成适配器 + 自动获取调度器 │ +│ • 提供商路由器(模型路由) │ +│ • TokenJuice 压缩 │ +│ • 原生工具(搜索、获取、文件系统、git…) │ +│ • 语音(STT 输入、TTS 输出、Meet Agent) │ +└──────────────────────────────────────────────────┘ + │ +┌──────────────────────────────────────────────────┐ +│ React 前端 (app/src/) │ +│ • 页面、导航 │ +│ • 通过 coreRpcClient 与核心通信 │ +│ • 无业务逻辑 —— 仅负责展示 │ +└──────────────────────────────────────────────────┘ +``` + +**逻辑归属:** + +* **Rust 核心**。所有业务逻辑。Memory Tree、集成、模型路由、工具、语音。具有权威性。 +* **Tauri 壳层**。窗口管理、进程生命周期、IPC。是交付载体,不是功能的栖身之所。 +* **React 前端**。UI 与编排。通过 JSON-RPC 调用核心。 + +## 数据流 + +1. **连接**。通过 OAuth 接入[集成](../../features/integrations/README.zh-CN.md)。后端保存 token;核心永远不会以明文形式看到它。 +2. **自动获取**。每二十分钟,[调度器](../../features/obsidian-wiki/auto-fetch.zh-CN.md)会遍历每个活跃连接,并要求每个原生提供商进行同步。 +3. **规范化**。提供商输出(邮件页面、GitHub diff、Slack 频道转储)被归一化为带来源标签的 Markdown。 +4. **分块**。Markdown 被拆分为 ≤3k token 的确定性块。 +5. **存储**。块存入 SQLite (`/memory_tree/chunks.db`),并以 `.md` 文件形式存入 `/wiki/`。 +6. **评分**。后台工作线程运行嵌入、实体提取、热度评分。 +7. **摘要**。从块池中构建并刷新来源 / 主题 / 全局摘要树。 +8. **检索**。当你提问时,Agent 查询 Memory Tree(搜索 / 钻取 / 主题 / 全局 / 获取)。 +9. **压缩**。工具输出和大型源数据在进入 LLM 上下文前经过 [TokenJuice](../../features/token-compression.zh-CN.md) 处理。 +10. **路由**。[路由器](../../features/model-routing/) 根据任务提示选择合适的提供商 + 模型。 + +## 隐私边界 + +留在你机器上的数据: + +* Memory Tree SQLite 数据库。 +* Obsidian Markdown 仓库。 +* 音频捕获缓冲区和任何本地模型状态。 + +经过 OpenHuman 后端的数据(在一个订阅下): + +* LLM 调用(模型提供商)。 +* 网页搜索智能体。 +* 集成 OAuth 和工具智能体。 +* TTS 流。 + +完整图景请参阅 [隐私与安全](../../features/privacy-and-security.zh-CN.md)。 + +## 开源 + +* **仓库:** [github.com/tinyhumansai/openhuman](https://github.com/tinyhumansai/openhuman)。GNU GPL3。 +* 欢迎提交 **Issue 和 PR**。项目处于早期测试阶段。 +* 对于贡献者,权威开发者指南是[深度架构文档](../architecture.zh-CN.md)。 diff --git a/gitbooks/developing/building-rust-core.zh-CN.md b/gitbooks/developing/building-rust-core.zh-CN.md new file mode 100644 index 0000000000..b595163dac --- /dev/null +++ b/gitbooks/developing/building-rust-core.zh-CN.md @@ -0,0 +1,190 @@ +--- +description: 在全新机器上从头构建 Rust 核心。 +icon: terminal +lang: zh-CN +--- + +# 构建 Rust 核心 + +本页面向贡献者,是在全新机器上编译 Rust 核心的参考文档。 + +它仅涵盖**仓库根目录的 crate**: + +- Cargo 包:`openhuman` +- 二进制文件:`openhuman-core` +- 库:`openhuman_core` + +如果你需要完整的桌面应用(`pnpm dev`、Tauri、CEF、前端工具链),请使用[环境搭建](getting-set-up.zh-CN.md)。该路径有额外的 JavaScript、子模块和桌面运行时依赖,**不**需要用于纯核心的 `cargo` 工作流。 + +## 1. 安装指定版本的 Rust 工具链 + +仓库在 [`rust-toolchain.toml`](../../rust-toolchain.toml) 中固定了 Rust 版本: + +- Channel:`1.93.0` +- Components:`rustfmt`、`clippy` + +推荐安装方式: + +```bash +rustup toolchain install 1.93.0 --component rustfmt --component clippy +rustup default 1.93.0 +``` + +你也可以在安装 `rustup` 后,让 `cargo` 从 `rust-toolchain.toml` 自动安装。 + +## 2. 克隆仓库 + +仅核心开发: + +```bash +git clone https://github.com/tinyhumansai/openhuman.git +cd openhuman +``` + +这对根目录 crate 来说已足够。 + +桌面/Tauri 开发则不同: + +- 只有在构建桌面壳层或 CEF 感知的 Tauri 工具链时,才需要 `app/src-tauri/vendor/` 子模块。 +- 该流程请遵循[环境搭建](getting-set-up.zh-CN.md)并运行 `git submodule update --init --recursive`。 + +## 3. 构建命令 + +从仓库根目录运行: + +```bash +# 快速依赖 + 类型检查 +cargo check --manifest-path Cargo.toml + +# 实际 CLI / RPC 二进制文件的 Debug 构建 +cargo build --manifest-path Cargo.toml --bin openhuman-core + +# Release 构建 +cargo build --manifest-path Cargo.toml --release --bin openhuman-core + +# Rust 测试 +cargo test --manifest-path Cargo.toml +``` + +注意: + +- **包**名是 `openhuman`,但可运行的二进制文件是 **`openhuman-core`**。 +- 如果你更喜欢面向包的 cargo 命令用于打包脚本,请使用 `-p openhuman`。 +- 构建好的二进制文件位于 `target/debug/openhuman-core` 或 `target/release/openhuman-core`。 + +## 4. macOS 前置条件 + +安装: + +- Xcode Command Line Tools:`xcode-select --install` + +原因: + +- `whisper-rs` 在构建期间编译原生代码。 +- 在 macOS 上,该 crate 在 [`Cargo.toml`](../../Cargo.toml) 中以 `metal` 特性启用构建,因此需要 Apple 工具链和 SDK 头文件。 + +安装 Xcode CLT 后,核心应该能用上述 cargo 命令构建。 + +## 5. Linux 前置条件 + +### 仅核心包集合 + +在全新 Linux 机器上运行 `cargo` 前,先安装这些包。 + +**Ubuntu / Debian:** + +```bash +sudo apt-get update +sudo apt-get install -y \ + build-essential cmake pkg-config clang libssl-dev libclang-dev \ + libasound2-dev libxi-dev libxtst-dev libxdo-dev libudev-dev \ + libstdc++-14-dev +``` + +**Arch Linux:** + +```bash +sudo pacman -S --needed base-devel cmake pkgconf clang openssl \ + alsa-lib libxi libxtst xdotool libevdev +``` + +> 在 Arch 上,`clang` 包含 `libclang`,`base-devel` 包含 `gcc`(提供 `libstdc++`),因此不需要单独的 `-dev` 包。 + +这些包的重要性: + +- `build-essential` / `base-devel`、`cmake`、`pkg-config` / `pkgconf`:传递性 Rust 依赖使用的原生构建。 +- `clang`、`libclang-dev`:bindgen / C 和 C++ 编译路径,被原生 crate 使用。 +- `libssl-dev` / `openssl`:某些网络依赖需要的 OpenSSL 头文件。 +- `libasound2-dev` / `alsa-lib`、`libxi-dev` / `libxi`、`libxtst-dev` / `libxtst`、`libxdo-dev` / `xdotool`、`libudev-dev`(Arch 中已包含在 `systemd-libs` 内)、`libevdev`:被核心构建引入的音频/输入/设备 crate 所需。 + +### `whisper-rs` + `clang` 注意事项 + +`whisper-rs-sys` 在 `clang` 下可能会失败并提示: + +```text +fatal error: 'array' file not found +``` + +这就是为什么文档特别指出 `libstdc++-14-dev`:`clang` 在 Ubuntu runner 上可能会选择 GCC 14 的 C++ 头文件。 + +如果你的发行版布局仍然导致构建无法解析 `libstdc++.so`,请使用 [`AGENTS.md`](../../AGENTS.md) 中记录的相同变通方案: + +```bash +# Ubuntu/Debian —— 按需调整 GCC 版本 +sudo ln -sf /usr/lib/gcc/x86_64-linux-gnu/13/libstdc++.so /usr/lib/x86_64-linux-gnu/libstdc++.so +``` + +Arch Linux 通常不需要此变通方案,因为 `gcc-libs` 将 `libstdc++.so` 放在了默认库搜索路径上。 + +### Linux 桌面/Tauri 包集合 + +如果你构建的是桌面壳层而非仅核心 crate,请安装更广泛的依赖集合。 + +**Ubuntu / Debian**(镜像自 [`.github/workflows/build-desktop.yml`](../../.github/workflows/build-desktop.yml)): + +```bash +sudo apt-get update +sudo apt-get install -y \ + libgtk-3-dev libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev \ + patchelf cmake libasound2-dev libxdo-dev libxtst-dev libx11-dev libxi-dev \ + libevdev-dev libssl-dev libclang-dev \ + libnss3 libnspr4 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 \ + libxkbcommon0 libxcomposite1 libxdamage1 libxfixes3 libxrandr2 \ + libgbm1 libpango-1.0-0 libcairo2 libatspi2.0-0 libxshmfence1 libu2f-udev +``` + +**Arch Linux:** + +```bash +sudo pacman -S --needed gtk3 webkit2gtk-4.1 libayatana-appindicator \ + librsvg patchelf nss nspr at-spi2-core libcups libdrm \ + libxkbcommon libxcomposite libxdamage libxfixes libxrandr \ + mesa pango cairo libxshmfence +``` + +仅在需要 `app/src-tauri/` 时使用桌面列表;对于根 crate 工作,上面较小的仅核心列表是相关的基线。 + +## 6. Windows 前置条件 + +安装: + +- 通过 `rustup` 安装 Rust +- Visual Studio Build Tools 2022 或带 **使用 C++ 的桌面开发** 工作负载的 Visual Studio +- CI 和发布构建使用的 MSVC 目标:`x86_64-pc-windows-msvc` + +安装 Microsoft 工具链后推荐的命令: + +```powershell +rustup toolchain install 1.93.0 --component rustfmt --component clippy +rustup target add x86_64-pc-windows-msvc +cargo build --manifest-path Cargo.toml --bin openhuman-core +``` + +Windows 注意事项: + +- 仓库对 `whisper-rs-sys` 打补丁以强制使用静态 MSVC CRT,并避免 [`Cargo.toml`](../../Cargo.toml) 中提到的 `LNK2038` / `LNK1169` 不匹配。请使用 MSVC 工具链,而非 MinGW。 + +## 7. 相关路径 + +- [环境搭建](getting-set-up.zh-CN.md):完整的桌面贡献者设置,含 `pnpm`、Tauri、子模块和 sidecar staging。 +- [OpenHuman 架构](architecture/README.zh-CN.md):核心在桌面应用和 RPC 流程中的位置。 diff --git a/gitbooks/developing/e2e-testing.zh-CN.md b/gitbooks/developing/e2e-testing.zh-CN.md new file mode 100644 index 0000000000..5216b1e36e --- /dev/null +++ b/gitbooks/developing/e2e-testing.zh-CN.md @@ -0,0 +1,256 @@ +--- +description: 使用 WDIO + tauri-driver / Appium 进行端到端测试。CI 和本地设置。 +icon: vials +lang: zh-CN +--- + +# E2E 测试指南 + +## 概述 + +桌面 E2E 测试使用 **WebDriverIO (WDIO)** 通过两个自动化后端驱动 Tauri 应用: + +| 平台 | 驱动 | 端口 | 应用格式 | 选择器 | +|----------|--------|------|------------|-----------| +| **Linux / CEF 状态** | `tauri-driver` | 4444 | Debug 二进制文件 | CSS / DOM | +| **macOS / Appium** | Appium Mac2 | 4723 | `.app` 包 | XPath / 辅助功能 | + +OpenHuman 桌面应用目前使用 CEF 运行时(`tauri-runtime-cef`)。Linux `tauri-driver` 与 WebKitWebDriver / webkit2gtk 通信,无法驱动 CEF -backed WebView,因此 Linux CEF E2E 在 CI 中被禁用,直到存在 CEF 兼容的驱动或替代 harness。目前支持的路径是 macOS/Appium 用于本地运行,以及在该工作流启用时手动触发 macOS/Appium 工作流运行。 + +--- + +## 快速开始 + +### Linux / CEF 状态 + +```bash +# 安装 tauri-driver(一次性) +cargo install tauri-driver + +# 构建 E2E 应用 +pnpm --filter openhuman-app test:e2e:build + +# 运行所有流程 +pnpm --filter openhuman-app test:e2e:all:flows + +# 运行单个 spec +bash app/scripts/e2e-run-spec.sh test/e2e/specs/smoke.spec.ts smoke +``` + +在无头 Linux 上,harness 在 **Xvfb** 虚拟显示下运行。此路径目前仅对非 CEF / WebKit 兼容调试有用;默认 CEF 应用无法被 WebKitWebDriver 自动化。 + +### macOS / Appium + +```bash +# 安装 Appium + Mac2 驱动(一次性,需要 Node 24+) +npm install -g appium +appium driver install mac2 + +# 构建 .app 包 +pnpm --filter openhuman-app test:e2e:build + +# 运行所有流程 +pnpm --filter openhuman-app test:e2e:all:flows +``` + +### macOS 上的 Docker(本地运行 Linux harness) + +使用 Docker 从 macOS 运行相同的基于 Linux 的 harness。同样的 CEF 限制适用:在存在 CEF 兼容驱动之前,这不是默认 CEF 运行时的支持路径。 + +```bash +# 构建 + 运行所有 E2E 流程 +docker compose -f e2e/docker-compose.yml run --rm e2e + +# 先构建应用(如需要) +docker compose -f e2e/docker-compose.yml run --rm e2e \ + pnpm --filter openhuman-app test:e2e:build + +# 运行单个 spec +docker compose -f e2e/docker-compose.yml run --rm e2e \ + bash app/scripts/e2e-run-spec.sh test/e2e/specs/smoke.spec.ts smoke +``` + +需要 Docker Desktop 或 Colima。仓库通过 bind mount 挂载,因此构建在运行之间持久化。 + +--- + +## 架构 + +### 平台检测 + +`app/test/e2e/helpers/platform.ts` 导出: + +- `isTauriDriver()`,`true` 表示 Linux(tauri-driver session) +- `isMac2()`,`true` 表示 macOS(Appium Mac2 session) +- `supportsExecuteScript()`,`true` 当 `browser.execute()` 可用时(仅 tauri-driver) + +### 元素辅助函数 + +`app/test/e2e/helpers/element-helpers.ts` 提供统一 API: + +| 辅助函数 | Mac2 (macOS) | tauri-driver (Linux) | +|--------|-------------|---------------------| +| `waitForText(text)` | @label/@value/@title 上的 XPath | DOM 文本内容上的 XPath | +| `waitForButton(text)` | XCUIElementTypeButton XPath | `button` / `[role="button"]` XPath | +| `clickText(text)` | W3C 指针动作 | 标准 `el.click()` | +| `clickNativeButton(text)` | XCUIElementTypeButton 上的 W3C 指针动作 | button 上的标准 `el.click()` | +| `clickToggle()` | XCUIElementTypeSwitch / XCUIElementTypeCheckBox | `[role="switch"]` / `input[type="checkbox"]` | +| `waitForWindowVisible()` | XCUIElementTypeWindow | 窗口句柄检查 | +| `waitForWebView()` | XCUIElementTypeWebView | `document.readyState` 检查 | +| `hasAppChrome()` | XCUIElementTypeMenuBar | 窗口句柄检查 | +| `dumpAccessibilityTree()` | 辅助功能 XML | HTML 页面源码 | + +### 稳定的测试 ID + +优先为 E2E spec 点击或轮询的 UI affordance 使用稳定的 `data-testid` hook。使用分类法 `--`,例如: + +- `cron-jobs-panel`、`cron-refresh` +- `cron-job-row-`、`cron-job-toggle-`、`cron-job-run-`、`cron-job-view-runs-`、`cron-job-remove-` +- `settings-nav-` +- `skill-row-`、`skill-install-`、`skill-uninstall-` +- `thread-row-`、`new-thread-button`、`send-message-button` +- `onboarding-next-button` + +当 spec 瞄准这些 hook 之一时,使用 `element-helpers.ts` 中的 `waitForTestId(testId)` 和 `clickTestId(testId)`。对行/动作发现保留文本选择器,对用户可见文案断言也保留文本选择器。 + +### 深度链接辅助函数 + +`app/test/e2e/helpers/deep-link-helpers.ts` 处理 auth 深度链接: + +- **tauri-driver**:`browser.execute(window.__simulateDeepLink(url))`(主要),`xdg-open`(备用) +- **Appium Mac2**:`macos: deepLink` 扩展命令(主要),`open -a ...`(备用) + +对于发布候选版,在触碰 CEF preflight、单实例或深度链接启动代码时,还要在 Linux 或 macOS 上运行一次手动 secondary-instance 冒烟测试: + +1. 正常启动 OpenHuman 并保持运行。 +2. 通过 OS opener 触发 `openhuman://auth?token=e2e-token&key=auth`。 +3. 确认已运行的窗口接收到回调,且不会启动第二个完整的 CEF 实例。 +4. 确认 secondary 进程干净退出,没有 CEF 缓存锁错误。 + +这捕捉了一类回归:secondary 进程在 Tauri 的深度链接转发路径安装之前,于 CEF 缓存 preflight 期间退出。 + +### 编写跨平台 spec + +1. 在 spec 中使用 `element-helpers.ts` 中的**辅助函数**,永远不要使用原始的 `XCUIElementType*` 选择器 +2. 使用 **`clickNativeButton(text)`** 代替内联 button-clicking 代码 +3. 使用 **`hasAppChrome()`** 代替检查 `XCUIElementTypeMenuBar` +4. 使用 **`waitForWebView()`** 代替检查 `XCUIElementTypeWebView` +5. 对于仅 macOS 的测试,使用 `process.platform` 守卫或单独的 spec 文件 +6. 对 hash 路由使用 `navigateViaHash(route)`;它等待 hash、`document.readyState` 和挂载的 React root 后返回。在 onboarding 之后,`walkOnboarding()` 也等待 `#/home` 加上 Home 页面标记,然后 spec 才会导航到别处。 + +--- + +## 环境变量 + +| 变量 | 默认值 | 说明 | +|----------|---------|-------------| +| `TAURI_DRIVER_PORT` | `4444` | tauri-driver WebDriver 端口 | +| `APPIUM_PORT` | `4723` | Appium 服务器端口 | +| `E2E_MOCK_PORT` | `18473` | Mock 后端服务器端口 | +| `OPENHUMAN_WORKSPACE` | (临时目录) | 应用工作区目录 | +| `OPENHUMAN_SERVICE_MOCK` | `0` | 启用服务 mock 模式 | +| `OPENHUMAN_E2E_MODE` | 未设置 | 启用破坏性测试支持 RPC;E2E runner 将其设为 `1` | +| `OPENHUMAN_E2E_AUTH_BYPASS` | 未设置 | 启用 JWT 绕过认证 | +| `DEBUG_E2E_DEEPLINK` | (verbose) | 设为 `0` 以静默深度链接日志 | +| `E2E_FORCE_CARGO_CLEAN` | 未设置 | E2E 构建前强制 cargo clean | + +--- + +## CI 工作流 + +### Push / PR 检查 + +默认的 `test.yml` 工作流运行前端单元测试和 Rust 检查。其 Linux `tauri-driver` E2E job 被注释掉了,因为 WebKitWebDriver 无法驱动 CEF-backed WebView。 + +被禁用的 Linux E2E job 过去会: +1. 安装系统依赖(webkit2gtk、Xvfb、dbus) +2. 通过 cargo 安装 `tauri-driver` +3. 用 mock 服务器 URL 构建应用 +4. 在 Xvfb 下运行所有 E2E 流程 + +### macOS / Appium + +macOS/Appium 是当前 CEF 桌面应用支持的自动化后端。在本地运行,或在该工作流启用时通过手动触发的 macOS 工作流运行: +1. 安装 Appium + Mac2 驱动 +2. 构建 `.app` 包 +3. 运行所有 E2E 流程 + +--- + +## 故障排除 + +### Linux:"WebView not ready" 超时 + +对于默认 CEF 运行时,这通常意味着不支持的 Linux `tauri-driver` 路径正试图通过 WebKitWebDriver 驱动 CEF-backed WebView。请使用 macOS/Appium,或等待 CEF 兼容的 Linux 驱动。 + +确保 `DISPLAY` 已设置且 Xvfb 正在运行: +```bash +export DISPLAY=:99 +Xvfb :99 -screen 0 1280x1024x24 & +``` + +还要确保 dbus 已启动(webkit2gtk 需要): +```bash +eval $(dbus-launch --sh-syntax) +``` + +### Linux:找不到 tauri-driver + +```bash +cargo install tauri-driver +``` + +### macOS:深度链接在 `tauri dev` 中不工作 + +深度链接需要 `.app` 包。请改用 `pnpm tauri build --debug --bundles app`。 + +### Docker:首次运行构建很慢 + +首次 Docker 构建从源码编译 Rust + tauri-driver。后续运行使用缓存层。Cargo registry 和 git 源通过 Docker volume 缓存。 + +## Spec:Notifications + +**文件**:`app/test/e2e/specs/notifications.spec.ts` + +通过实时 core sidecar 和 Notifications UI 页面测试 notification RPC 方法: + +- `notification_ingest`,通过 core RPC 创建新通知 +- `notification_list`,验证摄入的通知被返回 +- `notification_mark_read`,将通知标记为已读 +- `notification_stats`,检查聚合统计形状 +- UI:Notifications 页面渲染集成通知部分(`[data-testid="integration-notifications-section"]`) +- UI:Notifications 页面显示 System Events 部分(`[data-testid="system-events-section"]`) + +**运行**: + +```bash +bash app/scripts/e2e-run-spec.sh test/e2e/specs/notifications.spec.ts notifications +``` + +**平台说明**:RPC 测试(`notification_ingest`、`notification_list`、`notification_mark_read`、`notification_stats`)为 Linux/tauri-driver 和 macOS/Appium Mac2 编写,但默认 CEF 运行时的 Linux 执行被禁用,直到存在 CEF 兼容驱动。UI 断言(Notifications 页面部分)需要 `browser.execute()` 支持,因此当 `supportsExecuteScript()` 返回 `false` 时,它们在 Mac2 上自动跳过。 + +--- + +## Agent 可观测的工件流 + +对于一种规范的、可检查的 run,将截图、页面源码 dump 和 mock 请求日志写入磁盘: + +```bash +bash app/scripts/e2e-agent-review.sh +``` + +工件落在 `app/test/e2e/artifacts/-agent-review/`。完整详情 + 辅助 API:[`AGENT-OBSERVABILITY.md`](agent-observability.md)。任何失败的测试都会触发 `wdio.conf.ts` 的 `afterTest` hook,将 `failure-*.png` + `failure-*.source.xml` 写入同一运行目录。 + +--- + +## Rust 推理提供商 E2E + +这些测试(`tests/inference_provider_e2e.rs`)使用 **wiremock** 模拟 HTTP upstream,不需要实时 LLM API 调用。它们覆盖 OpenAI 兼容聊天、Anthropic 认证风格、每模型温度抑制、Ollama 本地提供商和 `/v1` HTTP 端点认证层。 + +```bash +# 本地: +bash scripts/test-rust-inference-e2e.sh + +# 通过 Docker(Linux,与 CI 相同镜像): +docker compose -f e2e/docker-compose.yml run --rm inference-e2e +``` diff --git a/gitbooks/developing/getting-set-up.zh-CN.md b/gitbooks/developing/getting-set-up.zh-CN.md new file mode 100644 index 0000000000..902ba17973 --- /dev/null +++ b/gitbooks/developing/getting-set-up.zh-CN.md @@ -0,0 +1,248 @@ +--- +description: 如何从源码构建 OpenHuman —— 工具链、 vendored Tauri CLI、sidecar staging。 +icon: wrench +lang: zh-CN +--- + +# 构建与安装 OpenHuman + +本指南涵盖完整的桌面/源码安装路径和发布安装包。 + +如果你只需要在新机器上运行仓库根目录的 Rust crate,请使用[构建 Rust 核心](building-rust-core.zh-CN.md)。该页面记录了固定的 Rust 工具链、OS 包前置条件以及 `openhuman-core` 的精确 `cargo` 命令。 + +本指南涵盖两条路径: + +1. 从源码构建并编译 OpenHuman +2. 安装最新的稳定发布二进制文件 + +## 前置条件 + +- `git` +- Node.js 24 或更高版本(见 `app/package.json`) +- `pnpm@10.10.0`(见根目录 `package.json` 的 `packageManager` 字段) +- 通过 `rustup` 安装的 Rust 1.93.0,含 `rustfmt` 和 `clippy`(见 `rust-toolchain.toml`) +- CMake,原生 Rust 依赖所需 +- `app/src-tauri/vendor/` 下的 Git 子模块,vendored CEF-aware Tauri CLI 所需 +- 平台桌面构建工具:macOS 上的 Xcode Command Line Tools,或 Linux 上的 Tauri GTK/WebKit/AppIndicator 包集合 + +macOS Homebrew 快速开始: + +```bash +brew install node@24 pnpm rustup-init cmake +rustup toolchain install 1.93.0 --profile minimal +rustup component add rustfmt clippy --toolchain 1.93.0 +``` + +Arch Linux 快速开始: + +```bash +sudo pacman -S --needed nodejs npm rustup cmake base-devel clang openssl \ + alsa-lib xdotool libxtst libxi libevdev gtk3 webkit2gtk-4.1 \ + libayatana-appindicator librsvg patchelf nss nspr at-spi2-core \ + libcups libdrm libxkbcommon libxcomposite libxdamage libxfixes \ + libxrandr mesa pango cairo libxshmfence +npm install -g pnpm@10.10.0 +rustup toolchain install 1.93.0 --profile minimal +rustup component add rustfmt clippy --toolchain 1.93.0 +``` + +## 从源码构建(本地编译) + +从仓库根目录运行: + +```bash +# 1) 克隆并进入仓库 +git clone https://github.com/tinyhumansai/openhuman.git +cd openhuman + +# 2) 获取 vendored Tauri/CEF 源码 +git submodule update --init --recursive + +# 3) 安装 JS 依赖(workspace) +pnpm install + +# 4) 构建 Rust 核心二进制文件 +cargo build --manifest-path Cargo.toml --bin openhuman-core + +# 5) 运行桌面 staging hook(当前为 no-op;为脚本兼容性保留) +cd app +pnpm core:stage + +# 6) 构建桌面应用产物 +pnpm build +``` + +本地开发(而非生产构建): + +```bash +# 仅 Web UI 开发:在上述 cd app 步骤后,在 app/ 内运行 +pnpm dev + +# 使用 vendored Tauri/CEF CLI 的桌面应用开发:从 workspace 根目录运行 +cd .. +pnpm --filter openhuman-app dev:app +``` + +## 安装最新稳定版(macOS/Linux x64) + +主要安装命令: + +```bash +curl -fsSL https://raw.githubusercontent.com/tinyhumansai/openhuman/main/scripts/install.sh | bash +``` + +安装器行为: + +- 解析你平台的最新稳定 OpenHuman 发布版本 +- 可用时验证产物摘要 +- 本地安装(默认不需要 sudo) +- macOS:将 `OpenHuman.app` 安装到 `~/Applications` +- Linux x64:将 AppImage 安装为 `~/.local/bin/openhuman` 并写入桌面入口 + +实用 flag: + +```bash +# 预览操作而不写入文件 +curl -fsSL https://raw.githubusercontent.com/tinyhumansai/openhuman/main/scripts/install.sh | bash -s -- --dry-run +``` + +## Windows(最新稳定版) + +使用 PowerShell: + +```powershell +irm https://raw.githubusercontent.com/tinyhumansai/openhuman/main/scripts/install.ps1 | iex +``` + +Windows 安装器行为: + +- 解析最新稳定版 +- 下载 x64 的 MSI/EXE +- 可用时验证摘要 +- 在安装包支持的情况下执行按用户安装 + +## ARM Linux 构建(aarch64) + +ARM Linux 构建由于 CEF 和 GTK 依赖需要特殊处理。 + +### 前置条件 + +```bash +# 安装 xvfb 用于 headless 构建/测试 +sudo apt install xvfb +``` + +### 构建 + +```bash +cd app +pnpm tauri build --target aarch64-unknown-linux-gnu +``` + +### 运行 ARM 二进制文件 + +该二进制文件需要设置 CEF 库路径: + +### 选项 1 —— 直接调用 + +```bash +REL_DIR=app/src-tauri/target/aarch64-unknown-linux-gnu/release +CEF_DIR=$(ls -d "$REL_DIR"/build/cef-dll-sys-*/out/cef_linux_aarch64 2>/dev/null | head -n1) +export LD_LIBRARY_PATH="$CEF_DIR:$REL_DIR/deps:$REL_DIR${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" +"$REL_DIR/OpenHuman" --no-sandbox +``` + +### 选项 2 —— Wrapper 脚本(推荐) + +保存到 `~/bin/openhuman` 并赋予可执行权限(`chmod +x ~/bin/openhuman`): + +```bash +#!/bin/bash +REL_DIR=/path/to/app/src-tauri/target/aarch64-unknown-linux-gnu/release +CEF_DIR=$(ls -d "$REL_DIR"/build/cef-dll-sys-*/out/cef_linux_aarch64 2>/dev/null | head -n1) +export LD_LIBRARY_PATH="$CEF_DIR:$REL_DIR/deps:$REL_DIR${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" +exec "$REL_DIR/OpenHuman" --no-sandbox "$@" +``` + +### DEB 包安装 + +```bash +DEB_FILE=$(ls app/src-tauri/target/aarch64-unknown-linux-gnu/release/bundle/deb/OpenHuman_*_arm64.deb | head -n1) +sudo dpkg -i "$DEB_FILE" +``` + +### GTK 初始化修复 + +ARM 构建需要 GTK 在 Tauri 创建系统托盘之前初始化。这在 `vendor/tauri-cef/crates/tauri-runtime-cef/src/lib.rs` 中处理: + +```rust +// CEF 初始化后,添加: +#[cfg(target_os = "linux")] +{ + gtk::init().ok(); +} +``` + +如果托盘初始化失败并提示 "GTK has not been initialized",请确保此修复已到位后重新构建。 + +全平台手动下载链接: + +- 网站:https://tinyhuman.ai/openhuman +- 最新发布:https://github.com/tinyhumansai/openhuman/releases/latest + +## 故障排除 + +### macOS:`pnpm dev:app` 退出并提示 "CEF cache is held by another OpenHuman instance" + +**症状** + +`pnpm dev:app`(或 Tauri 壳层的任何 debug 构建)在窗口出现前退出,提示类似: + +```text +[openhuman] CEF cache at /Users//Library/Caches/com.openhuman.app/cef is held by another OpenHuman instance (host , pid 12345). +Quit the running instance and try again. +Workaround: + pkill -f "OpenHuman.app/Contents" + pkill -f "openhuman-core" +``` + +**原因** + +CEF(Chromium Embedded Framework)通过 `~/Library/Caches/com.openhuman.app/cef` 下的 `SingletonLock` 符号链接对其用户数据目录持有独占锁。已安装的 `.app` 包和开发二进制文件使用相同的标识符(`com.openhuman.app`),因此它们无法并排运行。如果没有 preflight,`cef::initialize` 会返回失败,而 vendored `tauri-runtime-cef` 会以 Rust 回溯和无可操作消息的方式 panic(这是 preflight 落地前的 issue #864)。 + +**修复** + +退出另一个 OpenHuman 实例并重新运行。最快路径: + +```bash +pkill -f "OpenHuman.app/Contents" +pkill -f "openhuman-core" +pnpm dev:app +``` + +如果锁是由崩溃进程留下的(PID 已不存在),preflight 会自动移除陈旧的 `SingletonLock`,开发启动将继续,无需手动清理。 + +**已知限制** + +开发和发布构建仍然共享 `com.openhuman.app` 作为缓存标识符。将开发隔离到单独的 `com.openhuman.app.dev` 缓存需要修改 vendored `tauri-runtime-cef`(缓存路径在运行时内部从 bundle 标识符构建,未暴露给 openhuman 壳层)。作为 #864 的后续跟踪。 + +### 核心端口上的陈旧 `openhuman` RPC 进程 + +**症状** + +之前的 Tauri 构建或 `openhuman-core run` harness 在 `OPENHUMAN_CORE_PORT`(默认 `7788`)上留下了一个监听进程。在 issue #1130 之前,新的 Tauri 构建会静默附加到该监听器,导致版本漂移,以及新构建的 `OPENHUMAN_CORE_TOKEN` 不匹配时出现 401。 + +**当前行为(issue #1130)** + +`core_process::ensure_running` 现在在启动时探测端口: + +- 如果 `GET /` 将监听器识别为 OpenHuman 核心(JSON body 含 `"name": "openhuman"`),则将其视为之前运行的陈旧进程并主动终止(Unix 上 `SIGTERM`,750ms 后 `SIGKILL`;Windows 上 `taskkill /F /T /PID`)。Tauri 主机随后会生成自己的全新嵌入式核心。 +- 如果监听器是其他东西(或不讲 HTTP),启动会大声失败,并在日志中显示冲突,而非静默附加。 +- 设置 `OPENHUMAN_CORE_REUSE_EXISTING=1` 以选择回到遗留的 attach-to-anything 行为,在将 `openhuman-core run` 作为手动调试 harness 运行时很有用。 + +**手动清理(仍然有效)** + +```bash +pkill -f "OpenHuman.app/Contents" +pkill -f "openhuman-core" +``` diff --git a/gitbooks/developing/testing-strategy.zh-CN.md b/gitbooks/developing/testing-strategy.zh-CN.md new file mode 100644 index 0000000000..9f4c70cced --- /dev/null +++ b/gitbooks/developing/testing-strategy.zh-CN.md @@ -0,0 +1,157 @@ +--- +description: OpenHuman 如何测试其产品 —— Vitest、cargo test、WDIO E2E。每种测试该放哪里。 +icon: vial +lang: zh-CN +--- + +# 测试策略 + +OpenHuman 如何测试其产品。"我的测试该放哪里?"的权威答案。 companion 文档为 [`TEST-COVERAGE-MATRIX.md`](../../docs/TEST-COVERAGE-MATRIX.md)。 + +--- + +## 测试层级 + +| 层级 | 存放位置 | 测试内容 | 驱动方式 | +| -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | +| **Rust 单元测试** | 同一 `*.rs` 文件内的 `#[cfg(test)] mod tests`,或同级 `tests.rs`,或域名下的 `tests/` 子目录(例如 `src/openhuman/channels/tests/`) | 纯领域逻辑、schema、RPC handler 形态、内存状态机 | `cargo test` | +| **Rust 集成测试** | 仓库根目录的 `tests/*.rs` | 完整领域接线,含真实 Tokio 运行时、模拟外部服务、JSON-RPC 端到端(`tests/json_rpc_e2e.rs`)、领域 × 领域交互 | `pnpm test:rust`(调用 `bash scripts/test-rust-with-mock.sh`) | +| **Vitest 单元测试** | 与源码共存于 `app/src/**` 下的 `*.test.ts(x)`,或 `app/src/**/__tests__/` 下 | React 组件、hook、store slice、纯工具函数、service 层适配器 | `pnpm test:unit` | +| **WDIO E2E** | `app/test/e2e/specs/*.spec.ts` | 完整桌面流程:UI → Tauri → core sidecar → JSON-RPC;用户可见行为 | Linux CI: `tauri-driver`(端口 4444)。macOS 本地: Appium Mac2(端口 4723)。详见 [E2E 测试](e2e-testing.zh-CN.md)。 | +| **手动冒烟测试** | [`docs/RELEASE-MANUAL-SMOKE.md`](../../docs/RELEASE-MANUAL-SMOKE.md) | 驱动程序无法断言的 OS 级表面:TCC 权限弹窗、Gatekeeper、代码签名、DMG 安装、OS 原生通知 | 发布切割时由人工执行,在发布 PR 中签字确认 | + +--- + +## 决策树 —— 我的测试该放哪里? + +```text +变更是否在 JSON-RPC 边界之后(在 src/ 中)? +├─ 是 —— 是否跨领域或与外部服务通信? +│ ├─ 是 → Rust 集成测试 (tests/*.rs) +│ └─ 否 → Rust 单元测试(源码旁) +└─ 否 —— 变更在 app/ 中 + ├─ 是纯函数、hook、slice 或独立组件? + │ └─ 是 → Vitest 单元测试 (*.test.tsx 与源码共存) + └─ 是否用户可见 且 跨越 UI ⇄ Tauri ⇄ sidecar ⇄ JSON-RPC? + ├─ 是 → WDIO E2E (app/test/e2e/specs/*.spec.ts) + └─ 是否 OS 级(TCC、Gatekeeper、安装、OS 通知)? + └─ 是 → 手动冒烟清单 +``` + +如果一项变更触及多个层级,在**每个**触及的层级都写测试。不要用一层替代另一层。 + +--- + +## 失败路径要求 + +覆盖矩阵中的每个功能叶子节点,除了 happy path 外,**至少**还要有一个**失败 / 边界**断言。例如: + +- 文件写入工具:happy = 写入了字节;failure = 路径限制拒绝。 +- OAuth 流程:happy = 签发了 token;edge = 过期刷新 token 恢复。 +- 记忆存储:happy = 存储并召回;edge = 遗忘后再召回返回空。 + +只断言 happy path 的 spec 是不完整的。 + +--- + +## Mock 策略 + +- **单元 / 集成 / E2E 中禁止真实网络。** 使用共享 mock 后端(`scripts/mock-api-core.mjs`、`scripts/mock-api-server.mjs`、`app/test/e2e/mock-server.ts`)。 +- 测试用 admin 端点:`GET /__admin/health`、`POST /__admin/reset`、`POST /__admin/behavior`、`GET /__admin/requests`。 +- **外部服务**(Telegram、Slack、Gmail、Notion、Ollama、OpenAI 等)在 mock 后端层面被 stub;测试通过 `getRequestLog()` 断言请求形态。 +- 唯一可接受的例外是记录在案的发布切割手动冒烟步骤。 + +--- + +## 确定性规则 + +- 禁止 wall-clock 等待,使用 `waitForApp`、`waitForAppReady`、`waitForWebView` 辅助函数,或显式的元素就绪谓词。 +- 禁止共享文件系统状态,每个 E2E spec 在隔离的 `OPENHUMAN_WORKSPACE` 中运行(由 `app/scripts/e2e-run-spec.sh` 创建/清理)。 +- 禁止顺序依赖的 spec,每个 spec 必须能独立通过。 +- 禁止依赖绝对坐标或动画时序。 +- 禁止在 tauri-driver 上通过 `browser.keys()` 使用真实键盘,通过 `browser.execute(...)` 合成(参见 `command-palette.spec.ts` 中的模式)。 + +--- + +## 现有 harness 提供的能力 + +- **Mock 后端引导**:`app/test/e2e/mock-server.ts` 中的 `startMockServer` / `stopMockServer`。 +- **Auth 捷径**:`helpers/deep-link-helpers.ts` 中的 `triggerAuthDeepLink` / `triggerAuthDeepLinkBypass` 跳过真实 OAuth。 +- **元素辅助函数**:`helpers/element-helpers.ts` 中的 `clickNativeButton`、`waitForWebView`、`clickToggle`,在 spec 中使用这些代替原始的 `XCUIElementType*` 选择器。 +- **共享流程**:`helpers/shared-flows.ts` 中的 `completeOnboardingIfVisible`、`navigateViaHash`、`navigateToSkills`、`walkOnboarding`。 +- **从 spec 调用 Core RPC**:`helpers/core-rpc.ts` 中的 `callOpenhumanRpc`,当 UI 步骤可能脆弱时直接驱动 sidecar。 +- **平台守卫**:`helpers/platform.ts` 中的 `isTauriDriver`、`isMac2`、`supportsExecuteScript`。 +- **失败时捕获工件**:`captureFailureArtifacts` 从 `wdio.conf.ts` 运行,截图 + DOM dump 输出到 `app/test/e2e/artifacts/`。 + +--- + +## 命名与结构规范 + +- WDIO spec:端到端产品流用 `-flow.spec.ts`;更窄的表面用 `.spec.ts`。 +- Vitest 同位置:优先 `Component.tsx` + `Component.test.tsx` 同级;仅在组合多个相关测试时使用 `__tests__/`。 +- Rust 集成测试:文件名用 snake_case 匹配表面,JSON-RPC 驱动流用 `_e2e.rs`,跨领域用 `_integration.rs`。 +- 每个 `describe` / `mod tests` 块对应一个功能列表 ID 范围,如果映射不明显,在注释中链接矩阵行。 + +--- + +## 合并前门禁 + +开 PR 前运行。CI 会跑同一套,但本地更快: + +```bash +# Rust 核心 +cargo fmt --check +cargo check --manifest-path Cargo.toml +cargo clippy --manifest-path Cargo.toml -- -D warnings +cargo test --manifest-path Cargo.toml + +# Tauri 壳层 +cargo check --manifest-path app/src-tauri/Cargo.toml + +# 前端 +pnpm typecheck +pnpm lint +pnpm format:check +pnpm test:unit + +# 带 mock 后端的 Rust 集成测试 +pnpm test:rust + +# E2E(慢 —— 仅在行为用户可见变更时运行) +pnpm test:e2e:build +bash app/scripts/e2e-run-spec.sh test/e2e/specs/.spec.ts +``` + +--- + +## 无法被驱动程序自动化的 —— 需要手动冒烟 + +某些表面无法被 WDIO / Appium 驱动,因为它们跨越 OS 级信任边界或硬件路径。完整的清单 + 签字块位于 [`docs/RELEASE-MANUAL-SMOKE.md`](../../docs/RELEASE-MANUAL-SMOKE.md),该文件是每次发布必须验证内容的权威来源。涵盖示例: + +- macOS TCC 权限弹窗(辅助功能、输入监控、屏幕录制、麦克风) +- Gatekeeper 首次启动签名验证 +- 代码签名完整性(`codesign --verify --deep --strict`) +- DMG 安装 / 拖入 Applications 流程 +- 自动更新下载 + 重启 +- Linux OS 原生通知 toast(无显示服务器的 driver 无法看见 Xvfb 之外的 Linux) + +如果一项功能没有自动化覆盖,也不在手动冒烟清单上,视为未测试,开一个覆盖缺口。 + +--- + +## 覆盖矩阵即契约 + +[覆盖矩阵](../../docs/TEST-COVERAGE-MATRIX.md) 中的每个功能叶子节点映射到: + +1. 一个或多个测试路径,**或** +2. 一个合理的 `🚫` 并附手动冒烟条目。 + +当你添加 / 删除 / 重命名功能时,**在同一 PR 中更新矩阵行**。CI 将在 #965 落地后守卫此契约。 + +--- + +## 不确定时 + +- 尽可能把测试推到层级栈的**底层**(Rust 单元 > Rust 集成 > Vitest > WDIO)。更低层级更快、更确定、运行成本更低。 +- WDIO 用于真正跨越 UI ⇄ Tauri ⇄ sidecar ⇄ JSON-RPC 的行为。不要仅仅因为 UI 存在就通过 WDIO 驱动一个可单元测试的关注点。 +- 失败的 happy path 是回归。缺失的失败路径测试是缺口。两者都是 bug。