mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
docs(i18n): add zh-CN translations for developing modules (C2) (#2505)
Co-authored-by: agent:skill-master <skill-master@openclaw>
This commit is contained in:
co-authored by
agent:skill-master <skill-master@openclaw>
parent
3299c16933
commit
4a76f22b4d
@@ -0,0 +1,81 @@
|
||||
---
|
||||
description: 使 E2E 测试可调试的工件捕获层。日志、跟踪、截图。
|
||||
icon: eye
|
||||
---
|
||||
|
||||
# E2E 的 Agent 可观测性
|
||||
|
||||
本文档描述了使桌面应用可通过现有 WDIO/Appium/tauri-driver harness 被编码智能体(Codex、Claude Code、Cursor)检查的工件捕获层。
|
||||
|
||||
它有意保持精简:一个规范的 onboarding + 隐私流程,包含磁盘截图、页面源码 dump 和 mock 后端请求日志。更广泛的计划见仓库根目录的 `AGENT_OBSERVABILITY_PLAN.md`。
|
||||
|
||||
## TL;DR
|
||||
|
||||
```bash
|
||||
bash app/scripts/e2e-agent-review.sh
|
||||
```
|
||||
|
||||
工件落在:
|
||||
|
||||
```text
|
||||
app/test/e2e/artifacts/<ISO-timestamp>-agent-review/
|
||||
01-welcome.png
|
||||
01-welcome.source.xml
|
||||
02-post-welcome.png
|
||||
02-post-welcome.source.xml
|
||||
03-post-onboarding.png
|
||||
03-post-onboarding.source.xml
|
||||
04-privacy-panel.png
|
||||
04-privacy-panel.source.xml
|
||||
mock-requests-after-welcome.json
|
||||
mock-requests-after-onboarding.json
|
||||
mock-requests-after-privacy.json
|
||||
failure-<test>.png # 仅在失败时
|
||||
failure-<test>.source.xml # 仅在失败时
|
||||
meta.json # 运行元数据 + 检查点索引
|
||||
```
|
||||
|
||||
脚本最后会打印解析后的工件目录。
|
||||
|
||||
## 组成部分
|
||||
|
||||
| 组件 | 路径 | 作用 |
|
||||
|-------|------|------|
|
||||
| 辅助函数 | `app/test/e2e/helpers/artifacts.ts` | 运行目录、`captureCheckpoint`、`captureFailureArtifacts`、`saveMockRequestLog` |
|
||||
| WDIO hook | `app/test/wdio.conf.ts` (`afterTest`) | 任何失败测试都会 dump 截图 + 源码 |
|
||||
| 规范 spec | `app/test/e2e/specs/agent-review.spec.ts` | Welcome → onboarding → 隐私面板,带命名检查点 |
|
||||
| Wrapper 脚本 | `app/scripts/e2e-agent-review.sh` | 构建 + 运行 + 打印工件目录 |
|
||||
| 稳定选择器 | `OnboardingNextButton`、`Onboarding` 遮罩层 + 跳过按钮、`WelcomeStep`、`PrivacyPanel` 上的 `data-testid` | 智能体可靠的导航锚点 |
|
||||
|
||||
## 环境覆盖
|
||||
|
||||
| 变量 | 效果 |
|
||||
|----------|--------|
|
||||
| `E2E_ARTIFACT_DIR` | 强制指定运行目录(跳过自动时间戳命名) |
|
||||
| `E2E_ARTIFACT_ROOT` | 自动生成运行目录的父目录(默认:`app/test/e2e/artifacts`) |
|
||||
| `E2E_ARTIFACT_LABEL` | 自动生成的运行目录名中使用的标签(默认:`run`;wrapper 设为 `agent-review`) |
|
||||
|
||||
## 在新 spec 中使用辅助函数
|
||||
|
||||
```ts
|
||||
import {
|
||||
captureCheckpoint,
|
||||
saveMockRequestLog,
|
||||
} from '../helpers/artifacts';
|
||||
import { getRequestLog } from '../mock-server';
|
||||
|
||||
await captureCheckpoint('after-connect-click');
|
||||
saveMockRequestLog('after-connect-click', getRequestLog());
|
||||
```
|
||||
|
||||
`captureCheckpoint` 会对捕获进行编号,使运行目录按时间顺序阅读。
|
||||
`captureFailureArtifacts` 已接入 `wdio.conf.ts`,在任何失败测试中自动触发,spec 不应直接调用它。
|
||||
|
||||
## 有意排除的范围
|
||||
|
||||
- 跨每个组件状态的视觉基线 / 图像差异。
|
||||
- 每次点击都截图(太吵)。
|
||||
- 实时集成(Gmail、Notion、Telegram);仅 mock 服务器。
|
||||
- 新测试框架 / reporter。
|
||||
|
||||
仅在证明此循环有效后才扩展到更多流程。
|
||||
@@ -0,0 +1,311 @@
|
||||
---
|
||||
description: >-
|
||||
智能体轮次实际如何运行 —— 工具调用循环、子智能体分派、原型、分类、hook,以及围绕它们的成本/预算机制。
|
||||
icon: layer-group
|
||||
---
|
||||
|
||||
# Agent Harness
|
||||
|
||||
Agent Harness 是将用户消息(或 webhook 触发、cron tick)转变为完整的、使用工具的 LLM 交互的运行时。它拥有工具调用循环、子智能体分派、触发器-分类流水线和围绕它们的 hook 表面。它**不**拥有提供商 HTTP 传输、工具实现、提示部分组装或记忆存储 —— 那些是 harness 组合起来的独立领域。
|
||||
|
||||
本页先走过一个轮次中发生了什么,然后放大每个活动部件。
|
||||
|
||||
## 轮次的形态
|
||||
|
||||
每个轮次 —— 无论是用户刚输入消息、Telegram webhook 刚触发,还是 9am cron 刚 tick —— 都流经相同的生命周期:
|
||||
|
||||
```text
|
||||
┌─ 入站 ─────────────────────────────────────────────────────────┐
|
||||
│ 用户消息 · 渠道入站 · webhook · cron · composio 事件 │
|
||||
└──────────────────────────┬────────────────────────────────────────┘
|
||||
│
|
||||
▼ (仅外部触发器)
|
||||
┌──────────────────────┐
|
||||
│ 触发器分类 │ 分类 → 丢弃 / 通知 /
|
||||
│ (小型本地 LLM) │ 生成 reactor / 生成 orchestrator
|
||||
└──────────┬───────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────────┐
|
||||
│ Agent::turn() │
|
||||
│ 1. 恢复转录 │
|
||||
│ 2. 构建系统提示* │
|
||||
│ 3. 注入记忆上下文 │
|
||||
│ 4. 进入工具调用循环 ────┼──► 提供商调用
|
||||
│ 5. 分派工具调用 ────┼──► 工具执行 / 子智能体生成
|
||||
│ 6. 上下文守卫 / 压缩 │
|
||||
│ 7. 停止 hook 检查 │
|
||||
│ 8. 最终助手文本 │
|
||||
└──────────┬───────────────────┘
|
||||
│ 异步,在用户看到回复后
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ 轮次后 │ archivist · learning · 成本日志 ·
|
||||
│ hook │ 情景记忆索引
|
||||
└─────────────────┘
|
||||
|
||||
* 系统提示仅在第一轮构建 —— 后续轮次逐字复用渲染后的提示,
|
||||
以便推理后端的 KV-cache 前缀保持有效。
|
||||
```
|
||||
|
||||
本页其余部分就是同一个图表,展开版。
|
||||
|
||||
## 会话和 `Agent::turn`
|
||||
|
||||
**会话**是 `Agent` 实例正在运行的实时对话。`Agent` 结构体拥有:
|
||||
|
||||
* 对话历史(系统 + 用户 + 助手 + 工具消息)。
|
||||
* 要调用的提供商客户端(由[模型路由器](../../features/model-routing/)解析模型)。
|
||||
* 模型可见的工具注册表。
|
||||
* 在每条用户消息前为相关记忆补水的记忆加载器。
|
||||
* 每轮预算 —— 最大工具迭代次数、最大 payload 大小、最大 USD 成本。
|
||||
|
||||
`Agent::turn(user_message)` 是热路径。在一个轮次中它:
|
||||
|
||||
1. **恢复会话转录**,如果这是一个新进程 —— 从磁盘重新加载精确的提供商消息,以便推理后端的 KV-cache 前缀仍然命中。
|
||||
2. **构建系统提示**(仅在第一轮)。这拉入身份、soul、profile、记忆、已连接集成、可用工具、安全前言 —— 由提示部分构建器组装。
|
||||
3. **注入记忆上下文**,通过记忆加载器为新用户消息注入:[记忆树](../../features/obsidian-wiki/memory-tree.zh-CN.md) 中的相关块,附带引用,使 UI 可以展示来源。
|
||||
4. **进入工具调用循环**(下一节)。
|
||||
5. **在后台生成轮次后 hook** —— 用户在 archivist / learning / 成本日志完成前就得到答案。
|
||||
|
||||
系统提示在后续轮次中**不**重建。即使是微小的字节变化也会使 KV-cache 前缀失效并强制完整重新 prefill,因此动态每轮上下文(记忆召回、新学习片段)作为用户可见的消息内容追加,而非拼接到系统提示中。
|
||||
|
||||
## 工具调用循环
|
||||
|
||||
在 `Agent::turn` 内部,工具调用循环是内部引擎。它最多运行 `max_tool_iterations` 轮(默认 10):
|
||||
|
||||
```text
|
||||
loop {
|
||||
1. 上下文守卫 - 如果历史太长,microcompact / autocompact
|
||||
2. 停止 hook 检查 - 预算上限、最大迭代次数、自定义 kill switch
|
||||
3. 提供商调用 - 发送消息 + 工具 spec,流式响应
|
||||
4. 解析响应 - 将助手文本与工具调用分离
|
||||
5. 如果没有工具调用 - 返回最终文本
|
||||
6. 执行工具调用 - 分派每个(下一节)
|
||||
7. 总结超大结果 - 将巨大工具输出路由到 summarizer 智能体
|
||||
8. 追加结果 - 将工具结果推入历史,再次循环
|
||||
}
|
||||
```
|
||||
|
||||
每次迭代都会发出实时 `AgentProgress` 事件,以便 UI 可以逐 token 渲染流式传输、"正在调用工具 X" 状态和每轮成本更新。
|
||||
|
||||
### 工具分派和工具调用方言
|
||||
|
||||
不同的 LLM 说不同的工具调用方言。harness 通过 `ToolDispatcher` trait 抽象了这一点,它有三个具体实现:
|
||||
|
||||
* **Native** —— 拥有一等工具调用 API 的提供商(Anthropic、OpenAI)。工具调用以结构化字段返回,不在文本体中。
|
||||
* **XML** —— 未原生训练工具调用但可遵循指令的模型的 fallback。工具被包装在助手文本中的 `<tool_call>{...}</tool_call>` 标签内。
|
||||
* **P-Format** —— 某些较小模型使用的紧凑文本格式。
|
||||
|
||||
dispatcher 按提供商选择,使循环本身方言无关。相同的循环代码驱动 Claude、GPT、Gemini 和本地 Ollama 模型。
|
||||
|
||||
### 循环中的上下文管理
|
||||
|
||||
长工具调用链可能超出上下文窗口。两层处理:
|
||||
|
||||
* **工具结果预算** —— 每个工具结果都对照每调用字节预算检查。任何超出的内容都会被硬截断,并附带解释性标记,以便模型知道它没有看到完整输出。
|
||||
* **Microcompact / autocompact** —— 当总历史接近上下文窗口时,harness 在下次提供商调用前将旧轮次压缩为摘要。压缩后的历史保持系统提示和最近轮次不变(KV-cache 稳定性),并重写中间部分。
|
||||
|
||||
### 超大工具结果 —— summarizer 绕道
|
||||
|
||||
某些工具调用返回巨大的 payload —— Composio action dump 200 KB JSON、网页抓取返回 50 KB markdown、跨越数千行的日志上的 `file_read`。在 payload 中间硬截断会丢弃恰好落在截断点之后的任何内容。
|
||||
|
||||
当工具结果超过 summarizer 阈值时,它在进入父历史之前通过专用的 `summarizer` 子智能体路由。summarizer 按照保留标识符和关键事实的提取合约压缩 payload,父智能体只看到压缩后的摘要。当 summarization 失败或 payload 大到在其上支付 LLM 调用在经济上没有意义时,硬截断仍是下游的备用方案。
|
||||
|
||||
### 缺失命令的自愈
|
||||
|
||||
当代码执行器子智能体运行 shell 命令且运行时回答 "command not found" 时,自愈拦截器捕获错误,生成一个 `ToolMaker` 子智能体为缺失命令编写 polyfill 脚本,然后重试原始调用。每个命令有尝试上限,因此真正不可能的命令不会无限循环。
|
||||
|
||||
## 子智能体 —— orchestrator 模式
|
||||
|
||||
OpenHuman 是**多智能体**的。与用户聊天的智能体是 **Orchestrator** —— 一个高级别的、策略层面的智能体,决定何时直接回答、何时使用直接工具、何时生成专家子智能体。
|
||||
|
||||
### 为什么多智能体
|
||||
|
||||
一个知道一切的单个智能体也有一个小书大小的系统提示。将工作拆分到专家意味着:
|
||||
|
||||
* 每个子智能体获得一个**窄系统提示**,只有它需要的部分(可以剥离身份 / 记忆 / 安全前言)。
|
||||
* 每个子智能体获得一个**过滤后的工具注册表** —— 集成智能体不需要文件系统工具,coder 不需要 Composio 目录。
|
||||
* 子智能体历史永远不会泄露回父级 —— 父级看到一个紧凑的工具结果,而非内部对话。
|
||||
* 更便宜的模型可以做叶子工作。Orchestrator 使用强推理模型;研究子智能体可能使用更快、更便宜的模型。
|
||||
|
||||
### 内置原型
|
||||
|
||||
每个原型位于 `agents/<name>/` 下,带一个 `agent.toml`(元数据、工具范围、模型提示)和一个提示:
|
||||
|
||||
| 原型 | Orchestrator 何时选择它 |
|
||||
| ------------------- | --------------------------------------------------------------------------------------- |
|
||||
| `orchestrator` | 顶层智能体。永远不会被另一个 orchestrator 生成。 |
|
||||
| `planner` | 多步分解 —— 将复杂请求分解为有序子任务。 |
|
||||
| `researcher` | 网页/文档查找、引用搜寻。 |
|
||||
| `code_executor` | 在工作区中编写、运行和调试代码。 |
|
||||
| `critic` | 代码审查、对另一个智能体输出的质量检查。 |
|
||||
| `summarizer` | 压缩超大工具结果(由 harness 调用,通常不是模型调用)。 |
|
||||
| `archivist` | 记忆蒸馏 —— 持久化什么、遗忘什么。 |
|
||||
| `tool_maker` | 自愈 —— 为缺失的 shell 命令编写 polyfill。 |
|
||||
| `tools_agent` | 任意工具绑定任务的通用专家。 |
|
||||
| `integrations_agent`| 绑定到特定 Composio 工具包(Gmail、GitHub、Slack…)以执行该工具包的动作。|
|
||||
| `trigger_triage` | 将传入的外部事件分类为丢弃 / 通知 / 生成 reactor / 生成智能体。 |
|
||||
| `trigger_reactor` | 对分类后的触发器的轻量级反应,不需要完整的 orchestrator 轮次。 |
|
||||
| `morning_briefing` | 由 cron 运行的精选每日摘要。 |
|
||||
| `welcome` / `help` | Onboarding 流程。 |
|
||||
|
||||
自定义原型作为 TOML 文件发布在 `$OPENHUMAN_WORKSPACE/agents/*.toml`(或 `~/.openhuman/agents/*.toml` 用于用户全局专家)。自定义定义在 id 冲突时覆盖内置定义。
|
||||
|
||||
### 运行子智能体
|
||||
|
||||
当 orchestrator 调用 `spawn_subagent`(或 `delegate_*` 便捷工具之一)时,runner:
|
||||
|
||||
1. 从 task-local 读取父执行上下文 —— 父提供商、sandbox 模式、取消围栏、转录根。
|
||||
2. 解析子智能体的模型 —— 继承父级、遵循提示(`fast` / `reasoning` / `summarization`),或固定到精确模型。
|
||||
3. 按定义的 `tools`、`disallowed_tools` 和 `skill_filter` 过滤父级的工具注册表。在 `fork` 模式下,父级的完整注册表逐字继承。
|
||||
4. 构建窄系统提示,省略定义要求剥离的部分。
|
||||
5. 使用与父级相同的机制运行内部工具调用循环。
|
||||
6. 返回一个紧凑的文本结果。子智能体内部历史永远不会拼接到父级中 —— orchestrator 看到一个单一的工具结果并继续。
|
||||
|
||||
对于不需要阻塞 orchestrator 轮次的任务,`spawn_worker_thread` 在后台运行子智能体,orchestrator 立即继续。
|
||||
|
||||
### 生成层级和 tiers
|
||||
|
||||
并非每个智能体都被允许生成每个其他智能体。harness 建模了一个三层层级,镜像模型之间的成本 / 延迟 / 思考深度拆分:
|
||||
|
||||
```text
|
||||
Chat (快速,UX 聚焦 —— 例如 orchestrator 使用 `chat` 提示)
|
||||
│
|
||||
├─► Worker ◄─── 快速路径:一次委托,叶子做工作
|
||||
│
|
||||
└─► Reasoning (慢速,深度思考 —— 例如 planner 使用 `reasoning` 提示)
|
||||
│
|
||||
└─► Worker ◄─── 深度路径:reasoning 分解,workers 执行
|
||||
```
|
||||
|
||||
每个 `AgentDefinition` 携带一个 `agent_tier` 字段(`chat` / `reasoning` / `worker`,默认 `worker`)。契约:
|
||||
|
||||
| Tier | 可以生成 | 禁止生成 | 典型成员 |
|
||||
| ------------ | ----------------- | ---------------------------- | -------------------------------------------------------- |
|
||||
| `chat` | `reasoning`, `worker` | 另一个 `chat` | `orchestrator` |
|
||||
| `reasoning` | `worker` | 另一个 `reasoning`、任何 `chat` | `planner`(当今的规范代表) |
|
||||
| `worker` | nothing[^1] | 任何东西 | researcher、code_executor、critic、archivist、tool_maker、integrations_agent、… |
|
||||
|
||||
[^1]: Skill-wildcard 条目(`{ skills = "*" }`)被豁免,因为它们坍缩为单个 `delegate_to_integrations_agent` 工具,其目标是 worker —— 它们是扇出委托表面,不是递归生成。
|
||||
|
||||
**为什么有这些规则。**
|
||||
- *Chat → chat 毫无意义。* Chat tier 存在是为了 snappy UX。Chat 智能体生成另一个 chat 智能体只是加倍 TTFT 并燃烧 token 而不购买任何新能力。
|
||||
- *Reasoning → reasoning 会爆炸深度。* Reasoning tier 很昂贵。Reasoning 智能体链倾向于重新分解相同问题并创建失控的层级。
|
||||
- *Worker → anything 混合执行和编排。* Workers 是叶子,因此父级总是看到一个紧凑结果,而非嵌套委托的转录。
|
||||
|
||||
**强制执行。** 两层:
|
||||
|
||||
1. **加载时(静态)。** [`agents::loader::validate_tier_hierarchy`](../../../src/openhuman/agent/agents/loader.rs) 在合并的注册表(内置 + workspace TOML)上运行,并拒绝启动列出同级或 worker-with-subagents 条目的注册表。内置原型在编译测试时检查;用户发布的 TOML 在 workspace 加载时检查。
|
||||
2. **运行时深度门禁(动态)。** 独立于 tier,子智能体 runner 通过 task-local 计数器将总生成链深度限制为 `MAX_SPAWN_DEPTH = 3`,该计数器在 `run_subagent` 之间递增,作为 `SpawnDepthExceeded` 智能体错误展示。这使得一个删除了 tier 注释的用户发布 TOML 仍然无法递归超过三跳。
|
||||
|
||||
> **状态:** 加载时 tier 检查、`agent_tier` 字段和运行时深度计数器 task-local 已上线。深度由静态加载器契约和运行时 `MAX_SPAWN_DEPTH = 3` 守卫共同限制。
|
||||
|
||||
### 工具包特定专家
|
||||
|
||||
对于具有数百个动作的 Composio 工具包(仅 GitHub 就有 500+),将每个动作加载到子智能体的工具集中会膨胀提示大小。harness 通过廉价的纯 CPU 过滤器(动词检测、token 重叠、动词对齐提升)将工具包的动作与父级精炼的任务提示进行排名,并仅将排名靠前的子集加载到子智能体中。无需模型调用,纯启发式 —— 快速且可解释。
|
||||
|
||||
## 分类 —— 处理外部触发器
|
||||
|
||||
当 webhook 触发、cron tick 或 Composio 事件到达时,系统不能直接将它们交给 orchestrator。大多数触发器是噪音;有些值得通知;只有少数值得完整的智能体轮次。**触发器-分类流水线**是门禁。
|
||||
|
||||
```text
|
||||
TriggerEnvelope ──► run_triage ──► TriageDecision ──► apply_decision
|
||||
│ │
|
||||
│ ├─► 丢弃 (噪音)
|
||||
│ ├─► 仅通知
|
||||
│ ├─► 生成 trigger_reactor
|
||||
│ └─► 生成 orchestrator
|
||||
│
|
||||
└── 小型本地 LLM(云端 LLM 重试 fallback)
|
||||
```
|
||||
|
||||
evaluator 有意保持廉价 —— 在可用时使用小型本地模型,重试时 fallback 到远程模型。决策被缓存,因此相同的触发器不会重新分类。只有升级到"生成 orchestrator"的触发器才会通过完整的 `Agent::turn` 机制。
|
||||
|
||||
## Hook —— 可观测性和策略杠杆
|
||||
|
||||
两个 hook 表面包裹循环,位于两端:
|
||||
|
||||
### 停止 hook(轮次中)
|
||||
|
||||
停止 hook 在工具调用循环的**迭代之间**触发。它们是预算上限、速率限制和自定义 kill switch 的策略杠杆。内置 hook:
|
||||
|
||||
* **预算停止 hook** —— 使用每轮成本累加器限制轮次的累计 USD 成本。
|
||||
* **最大迭代次数停止 hook** —— 从智能体持久配置外部限制迭代次数。
|
||||
|
||||
返回 `Stop` 的 hook 会以清晰的原因中止循环,调用者可以将该原因展示给用户。停止 hook 与中断(下一节)不同:它们是策略驱动的,不是用户驱动的。
|
||||
|
||||
### 轮次后 hook
|
||||
|
||||
轮次后 hook 在轮次**完成后**触发,在后台。它们获得 `TurnContext` 快照 —— 用户消息、助手响应、每个工具调用及其参数和结果、总 wall-clock、迭代次数、会话 ID。内置消费者:
|
||||
|
||||
* **Archivist** —— 蒸馏轮次中哪些事实值得持久化到长期记忆。
|
||||
* **Learning** —— 为 reflection、工具跟踪器和用户 profile 更新提供输入。
|
||||
* **成本日志** —— 最终每轮成本行。
|
||||
* **情景记忆索引** —— 将轮次作为块写入[记忆树](../../features/obsidian-wiki/memory-tree.zh-CN.md)以供未来召回。
|
||||
|
||||
Hook 通过 `tokio::spawn` 运行,因此用户在它们完成前就得到了答案。
|
||||
|
||||
## 中断 —— 优雅取消
|
||||
|
||||
`InterruptFence` 在循环的固定安全点检查 —— 每次工具执行前、每次子智能体生成前、每次提供商调用前。当用户按下 Ctrl+C 或发送 `/stop`:
|
||||
|
||||
* 围栏翻转。
|
||||
* 每个正在运行的子智能体看到相同的 flag(通过 `Arc` 共享)并在其下一个检查点退出。
|
||||
* 进行中的提供商流被丢弃。
|
||||
* Archivist 仍然使用任何存在的部分上下文触发,因此对话不会丢失。
|
||||
|
||||
中断是用户驱动的;停止 hook 是策略驱动的。它们共享底层的"干净停止循环"管道,但从不同侧面进入。
|
||||
|
||||
## 成本核算
|
||||
|
||||
每个提供商响应携带一个 `UsageInfo` 块 —— 输入 token、输出 token、缓存输入 token,以及由 OpenHuman 后端填充的权威 `charged_amount_usd`。`TurnCost` 在一个轮次内对每个提供商调用求和,以便 harness 可以:
|
||||
|
||||
* 通过进度通道发出每轮成本遥测。
|
||||
* 为预算停止 hook 提供输入,使失控的轮次在循环中自我切断。
|
||||
* 记录精确的轮次结束成本行。
|
||||
|
||||
当后端不展示收费金额时(旧构建、不通过它计费的提供商),一个小的每 tier 费率表提供 token 费率 floor 估计。后端直接成本在可用时总是优先。
|
||||
|
||||
## Fork 上下文 —— 跨 harness 的 KV-cache 复用
|
||||
|
||||
harness 使用 task-local `ParentExecutionContext` 将父状态线程化到子智能体中,而不会爆炸每个函数签名。相同的模式携带当前 sandbox 模式、中断围栏和停止 hook 列表。继承父级提供商、模型和提示前缀的子智能体可以在推理后端上**共享父级的 KV-cache 前缀** —— 比从头重新 prefill 明显更便宜。
|
||||
|
||||
## 自愈回顾
|
||||
|
||||
几个小型自适应系统位于主循环之上:
|
||||
|
||||
* **缺失命令的自愈** —— `ToolMaker` polyfill,有上限的重试尝试。
|
||||
* **Payload summarizer 断路器** —— 会话中连续三次子智能体失败会禁用 summarization,fallback 到截断。
|
||||
* **分类本地-vs-远程重试** —— 本地 LLM 优先;解析失败时远程 fallback。
|
||||
|
||||
这些都不会改变循环的形状 —— 它们只是让常见故障模式无需用户干预即可恢复。
|
||||
|
||||
## 代码中该看哪里
|
||||
|
||||
harness 完全位于 `src/openhuman/agent/` 下。该目录中的 README 枚举了公共表面;负载最重的文件是:
|
||||
|
||||
| 文件 / 目录 | 里面有什么 |
|
||||
| ----------------------------- | ----------------------------------------------------------------- |
|
||||
| `harness/session/turn.rs` | `Agent::turn` —— 上述生命周期。 |
|
||||
| `harness/tool_loop.rs` | 内部工具调用循环。 |
|
||||
| `harness/subagent_runner/` | `run_subagent`、fork 模式、超大结果交接。 |
|
||||
| `harness/definition.rs` | `AgentDefinition` —— 原型声明的内容。 |
|
||||
| `harness/tool_filter.rs` | 集成子智能体的工具包动作排名。 |
|
||||
| `harness/payload_summarizer.rs` | 超大工具结果绕道。 |
|
||||
| `harness/self_healing.rs` | 缺失命令拦截器。 |
|
||||
| `harness/interrupt.rs` | 取消围栏。 |
|
||||
| `dispatcher.rs` | 工具调用方言抽象。 |
|
||||
| `triage/` | 外部触发器分类 + 升级。 |
|
||||
| `agents/` | 内置原型 —— 每个智能体一个子目录。 |
|
||||
| `hooks.rs` / `stop_hooks.rs` | 轮次后和轮次中 hook 表面。 |
|
||||
| `cost.rs` | 每轮 USD/token 核算。 |
|
||||
| `progress.rs` | 到 UI 的实时进度事件。 |
|
||||
| `memory_loader.rs` | 每条用户消息的记忆树上下文注入。 |
|
||||
|
||||
## 另请参阅
|
||||
|
||||
* [架构概览](README.zh-CN.md) —— harness 在更大图景中的位置。
|
||||
* [记忆树](../../features/obsidian-wiki/memory-tree.zh-CN.md) —— 记忆加载器从中读取、轮次后 hook 写入的内容。
|
||||
* [自动模型路由](../../features/model-routing/README.zh-CN.md) —— `model: "hint:reasoning"` 如何解析为具体的提供商+模型。
|
||||
* [原生工具 —— 智能体协调](../../features/native-tools/agent-coordination.zh-CN.md) —— `spawn_subagent`、`delegate_*`、`todo_write` 的用户可见表面。
|
||||
@@ -0,0 +1,129 @@
|
||||
---
|
||||
description: Desktop Companion 领域 —— Clicky 风格的交互循环,将热键、语音、屏幕智能、LLM、TTS 和视觉指向整合为单一产品体验。
|
||||
icon: robot
|
||||
---
|
||||
|
||||
# Desktop Companion (`src/openhuman/desktop_companion/`)
|
||||
|
||||
Desktop Companion 编排一个 Clicky 风格的交互循环:热键激活、麦克风捕获、屏幕上下文、LLM 推理、语音合成和视觉指向。它复用现有构建块,而非重新实现它们。
|
||||
|
||||
## 构建块
|
||||
|
||||
| 模块 | 提供的能力 | 路径 |
|
||||
|--------|-----------------|------|
|
||||
| **screen_intelligence** | 权限门控的捕获会话、`capture_now()`、`VisionSummary`、`AppContextInfo` | `src/openhuman/screen_intelligence/` |
|
||||
| **voice** | 热键监听器(push/tap)、音频捕获、云端 STT(Whisper)、TTS (`reply_speech`) | `src/openhuman/voice/` |
|
||||
| **meet_agent** | LLM 编排模式(STT -> LLM -> TTS)、WAV 打包 | `src/openhuman/meet_agent/` |
|
||||
| **overlay** | 浮动 UI 表面、注意力事件、打字机气泡 | `src/openhuman/overlay/` |
|
||||
| **provider_surfaces** | 连接应用事件队列 (`ingest_event`, `list_queue`) | `src/openhuman/provider_surfaces/` |
|
||||
| **accessibility** | 前台应用上下文 (`foreground_context()`) | `src/openhuman/accessibility/` |
|
||||
|
||||
## 模块布局
|
||||
|
||||
```text
|
||||
src/openhuman/desktop_companion/
|
||||
mod.rs — 模块导出(轻量)
|
||||
types.rs — CompanionState enum、CompanionConfig、ConversationTurn、会话 param/result 类型
|
||||
session.rs — 单例会话生命周期、状态机、TTL、对话历史
|
||||
pipeline.rs — STT -> 屏幕上下文 -> LLM -> TTS -> 指向编排
|
||||
pointing.rs — [POINT:x,y:label:screenN] 标签解析器、多显示器坐标映射
|
||||
handoff.rs — 连接应用动作的 provider-surface 队列匹配
|
||||
bus.rs — CompanionStateChangedEvent 的广播通道
|
||||
schemas.rs — RPC 控制器 (companion_start_session, companion_stop_session 等)
|
||||
```
|
||||
|
||||
## 状态机
|
||||
|
||||
```text
|
||||
Idle -> Listening -> Thinking -> Speaking -> Pointing -> Idle
|
||||
| |
|
||||
v v
|
||||
Listening Listening (中断)
|
||||
|
||||
任何状态 -> Error -> Idle (重置)
|
||||
```
|
||||
|
||||
有效转换由 `session::is_valid_transition()` 强制执行。关键路径:
|
||||
|
||||
- **Happy path**:Idle -> Listening -> Thinking -> Speaking -> Pointing -> Idle
|
||||
- **无指向**:Thinking -> Speaking -> Idle(响应中没有 POINT 标签)
|
||||
- **中断**:Speaking/Pointing -> Listening(用户重新激活热键)
|
||||
- **取消**:Thinking -> Idle(用户在思考中途取消)
|
||||
- **错误恢复**:Any -> Error -> Idle
|
||||
|
||||
## 交互流水线
|
||||
|
||||
`pipeline.rs` 编排单个轮次:
|
||||
|
||||
1. **激活** —— 状态转换为 Listening(将由 Tauri 壳层热键桥接驱动,见 PR 2)
|
||||
2. **STT** —— 通过 `voice::cloud_transcribe`(Whisper)转录音频样本
|
||||
3. **屏幕上下文** —— `accessibility::foreground_context()` 获取应用名称 + 窗口标题
|
||||
4. **LLM** —— 通过 `BackendOAuthClient` 进行聊天补全,携带系统提示、屏幕上下文和滚动对话历史(最近 20 轮作为上下文)
|
||||
5. **解析响应** —— 通过 `pointing::parse_and_map()` 提取 `[POINT:x,y:label:screenN]` 标签
|
||||
6. **Handoff 检查** —— 扫描响应中的提供商关键词,与 `provider_surfaces` 队列匹配
|
||||
7. **TTS** —— 通过 `voice::reply_speech`(ElevenLabs)合成语音
|
||||
8. **指向** —— 为 overlay 动画发射指向目标
|
||||
9. **返回 Idle**
|
||||
|
||||
流水线通过 `CancellationToken` 支持取消 —— Tauri 壳层可以在任何检查点取消(STT、LLM、TTS 阶段之间)。
|
||||
|
||||
文本输入也通过 `run_text_turn()` 支持,跳过 STT。
|
||||
|
||||
## 会话生命周期
|
||||
|
||||
- **一次一个会话** —— 由进程级 `Mutex<Option<CompanionSessionInner>>` 强制执行
|
||||
- **需要同意** —— `start_session` 拒绝 `consent=false`
|
||||
- **TTL 强制执行** —— 当 `status()` 检测到 TTL 已过时,会话自动过期
|
||||
- **对话历史** —— 上限 50 轮,溢出时最旧的被丢弃
|
||||
|
||||
## RPC 表面
|
||||
|
||||
命名空间:`companion`。所有方法都通过标准控制器注册表。
|
||||
|
||||
| 方法 | 说明 |
|
||||
|--------|-------------|
|
||||
| `companion_start_session` | 以显式同意 + 可选 TTL 启动会话 |
|
||||
| `companion_stop_session` | 结束活跃会话 |
|
||||
| `companion_status` | 当前状态、会话信息、剩余 TTL |
|
||||
| `companion_config_get` | 读取 companion 配置 |
|
||||
| `companion_config_set` | 更新 companion 配置 |
|
||||
|
||||
## 事件总线
|
||||
|
||||
`CompanionStateChangedEvent` 通过 `tokio::sync::broadcast` 通道广播(与 `overlay::bus` 相同模式)。三个 `DomainEvent` 变体路由到 `"companion"` 领域:
|
||||
|
||||
- `CompanionSessionStarted { session_id }`
|
||||
- `CompanionStateChanged { session_id, state, previous_state }`
|
||||
- `CompanionSessionEnded { session_id, reason }`
|
||||
|
||||
## 指向系统
|
||||
|
||||
LLM 响应可以嵌入 `[POINT:x,y:label:screenN]` 标签。`pointing.rs`:
|
||||
|
||||
- 通过正则解析标签
|
||||
- 使用 `ScreenGeometry` 将屏幕相对坐标映射为绝对桌面坐标
|
||||
- 将坐标钳制到屏幕边界
|
||||
- 索引越界时回退到 screen 0
|
||||
- 从显示文本中剥离标签
|
||||
|
||||
## Provider-surface handoff
|
||||
|
||||
`handoff.rs` 扫描清理后的 LLM 响应文本中的提供商关键词(slack、discord、telegram 等),并将它们与 `provider_surfaces` 队列中的条目匹配。当找到匹配时,`HandoffEvent` 被包含在 `TurnResult` 中,供 Tauri 壳层 / overlay 展示。
|
||||
|
||||
## 平台范围
|
||||
|
||||
- **macOS**:完整支持 —— 热键、屏幕捕获、指向、TTS、overlay
|
||||
- **Windows/Linux**:部分 —— 热键可用(rdev),屏幕上下文 stub,无指向
|
||||
|
||||
平台特定代码通过 `#[cfg(target_os = "macos")]` 门控。
|
||||
|
||||
## 测试
|
||||
|
||||
| 文件 | 覆盖范围 |
|
||||
|------|----------|
|
||||
| `session_tests.rs` | 会话 CRUD、状态机转换、TTL、同意、对话历史 |
|
||||
| `pipeline_tests.rs` | 轮次编排、取消、输入验证、系统提示 |
|
||||
| `pointing_tests.rs` | 标签解析、坐标映射、多显示器、边界情况 |
|
||||
| `handoff.rs` (inline) | 关键词匹配、空队列、提供商覆盖 |
|
||||
| `schemas.rs` (inline) | 控制器计数、schema 字段验证 |
|
||||
| `tests/json_rpc_e2e.rs` | 完整 RPC 往返:start -> status -> config -> stop |
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,209 @@
|
||||
---
|
||||
description: 桌面宿主 (`app/src-tauri/`) —— Tauri v2 + WebView、IPC、sidecar 生命周期、核心桥接。
|
||||
icon: desktop
|
||||
---
|
||||
|
||||
# Tauri Shell (`app/src-tauri/`)
|
||||
|
||||
OpenHuman 的桌面宿主:Tauri v2 + WebView、IPC 命令、窗口管理,以及桥接到 `openhuman-core` Rust sidecar(核心 JSON-RPC)。它**不会**重复完整的领域栈;那部分存在于仓库根目录的 Rust crate 中(`openhuman_core`、`src/main.rs`)。
|
||||
|
||||
## 职责
|
||||
|
||||
1. **Web UI**。从 `app/dist` 加载 Vite 构建(或开发服务器,端口 1420)。
|
||||
2. **IPC**。暴露一小套明确的 Tauri 命令(见 [Commands](#tauri-ipc-commands-app-src-tauri))。
|
||||
3. **核心生命周期**。确保 `openhuman-core` 二进制文件正在运行(子进程和/或服务)并通过 `core_rpc_relay` 代理 JSON-RPC。
|
||||
4. **磁盘上的 AI 提示**。从资源 / 开发 cwd 解析捆绑的 `src/openhuman/agent/prompts`,用于 `ai_get_config` / `write_ai_config_file`。
|
||||
5. **窗口 + 托盘**。桌面窗口行为和系统托盘(见 `lib.rs`)。
|
||||
|
||||
## 构建 sidecar
|
||||
|
||||
`app/package.json` 的 `core:stage` 运行 `scripts/stage-core-sidecar.mjs`,后者在仓库根目录运行 `cargo build --bin openhuman-core` 并将二进制文件复制到 `app/src-tauri/binaries/`,供 Tauri `externalBin` 使用。
|
||||
|
||||
## 卡死进程恢复
|
||||
|
||||
正常应用退出从 `RunEvent::ExitRequested` 运行 teardown:CEF 关闭前先关闭子 webview,触发嵌入式核心的 cancellation token,最终进程扫描在短暂的宽限期后向直接子进程发送 `SIGTERM`,然后升级使用 `SIGKILL` 处理顽固进程。扫描摘要记录为 `[app] sweep: term=N kill=M total=K`;任何非零 `kill` 计数都是警告,意味着子进程忽略了优雅关闭。
|
||||
|
||||
在 macOS 上,硬退出(强制退出、`SIGKILL`、渲染器崩溃)可能跳过正常的 teardown。下一次启动在 CEF 缓存 preflight 之前运行启动恢复:它列出可执行路径属于正在启动的 `.app/Contents` 的 OpenHuman 进程,跳过当前进程,发送 `SIGTERM`,短暂等待,然后对仍然匹配相同 pid+command 的顽固进程发送 `SIGKILL`。日志使用 `[startup-recovery]` 前缀。
|
||||
|
||||
当设置了 `OPENHUMAN_CORE_REUSE_EXISTING=1` 时(以便手动 CLI-core 复用仍然有效),以及当 CEF `SingletonLock` 被实时进程持有时(以便正常的 second-instance 路径可以在不杀死已运行应用的情况下失败),启动恢复跳过。Tauri 命令 `process_diagnostics_list_owned` 返回当前拥有的进程列表;macOS 实现是 bundle 作用域的,Linux/Windows 目前返回空。
|
||||
|
||||
|
||||
## Tauri Shell 架构 (`app/src-tauri/`)
|
||||
|
||||
### 概述
|
||||
|
||||
**`app/src-tauri`** crate(Rust 包 **`OpenHuman`**,二进制文件 **`OpenHuman`**)是一个**仅限桌面**的宿主。它嵌入 React UI,注册插件(深度链接、打开器、OS、通知、自动启动、更新器),管理主窗口和托盘,并**中继 JSON-RPC** 到单独构建的 **`openhuman-core`** 二进制文件。
|
||||
|
||||
非桌面目标在编译时失败(`lib.rs` 中的 `compile_error!`)。
|
||||
|
||||
### 目录布局(实际)
|
||||
|
||||
```text
|
||||
app/src-tauri/src/
|
||||
├── lib.rs # `run()`、托盘/菜单动作、插件、`generate_handler!`、核心启动
|
||||
├── main.rs # 二进制入口
|
||||
├── core_process.rs # CoreProcessHandle、生成/监控 openhuman sidecar
|
||||
├── core_rpc.rs # 核心 JSON-RPC 的 HTTP 客户端
|
||||
├── commands/
|
||||
│ ├── mod.rs # 重新导出
|
||||
│ ├── core_relay.rs # `core_rpc_relay`、服务管理的核心引导
|
||||
│ ├── openhuman.rs # Daemon 宿主配置、systemd 风格服务辅助函数
|
||||
│ └── window.rs # 显示/隐藏/最小化/关闭窗口
|
||||
└── utils/
|
||||
├── mod.rs
|
||||
└── dev_paths.rs # 解析捆绑的 AI 提示路径
|
||||
```
|
||||
|
||||
此树中**没有** `src-tauri/src/services/session_service.rs`;会话语义在 Web 层 + 后端 + 核心中按适用情况处理。
|
||||
|
||||
### 数据流:UI → 核心
|
||||
|
||||
```text
|
||||
React (invoke)
|
||||
→ core_rpc_relay { method, params, serviceManaged? }
|
||||
→ core_rpc::call HTTP POST 到 OPENHUMAN_CORE_RPC_URL
|
||||
→ openhuman 二进制文件 (src/bin/openhuman.rs → core_server)
|
||||
```
|
||||
|
||||
`core_process.rs` 中的 `CoreProcessHandle` 启动或等待 sidecar;`commands/core_relay.rs` 可选地在 relay 之前确保**服务管理**的核心正在运行。
|
||||
|
||||
### 窗口和托盘行为
|
||||
|
||||
- 壳层在启动时创建托盘图标,并将动作连接到打开主窗口或退出。
|
||||
- 在 daemon 模式(`daemon` / `--daemon`)下,主窗口在启动时隐藏,可以从托盘动作重新打开。
|
||||
- 在 macOS 上,`RunEvent::Reopen` 也会恢复并聚焦主窗口。
|
||||
- Windows 和 Linux 使用相同的托盘动作(`Open OpenHuman`、`Quit`),某些 Linux 设置上有桌面环境特定的托盘渲染差异。
|
||||
|
||||
### 捆绑资源
|
||||
|
||||
`tauri.conf.json` 捆绑 **`../../skills/skills`** 和 **`../../src/openhuman/agent/prompts`**,使技能和提示 markdown 随应用一起发布。
|
||||
|
||||
### 相关
|
||||
|
||||
- IPC 表面:见下方的 [Commands](#tauri-ipc-commands-app-src-tauri) 部分
|
||||
- HTTP 桥接:见下方的 [Core bridge & helpers](#core-bridge-helpers-app-src-tauri) 部分
|
||||
- Rust 领域(实现):仓库根目录 `src/openhuman/`、`src/core_server/`
|
||||
|
||||
|
||||
## Tauri IPC 命令 (`app/src-tauri`) {#tauri-ipc-commands-app-src-tauri}
|
||||
|
||||
所有命令都在 **`app/src-tauri/src/lib.rs`** 中的 `tauri::generate_handler![...]` 内注册(桌面构建)。下方名称是 **Rust** 命令名称(在 JS 中通过 serde 应用 camelCase)。
|
||||
|
||||
### Demo / 诊断
|
||||
|
||||
| 命令 | 用途 |
|
||||
| ------- | ------------------------------------------ |
|
||||
| `greet` | Demo 字符串(生产中可安全移除) |
|
||||
|
||||
### AI 配置(捆绑提示)
|
||||
|
||||
| 命令 | 用途 |
|
||||
| ---------------------- | -------------------------------------------------------------------------------------------- |
|
||||
| `ai_get_config` | 从捆绑或开发 `src/openhuman/agent/prompts` 下解析的 `SOUL.md` / `TOOLS.md` 构建 `AIPreview` |
|
||||
| `ai_refresh_config` | 与 `ai_get_config` 相同的读取路径(刷新 hook) |
|
||||
| `write_ai_config_file` | 在仓库 `src/openhuman/agent/prompts` 下写入单个 `.md`(开发 / 安全文件名检查) |
|
||||
|
||||
### 核心 JSON-RPC 中继
|
||||
|
||||
| 命令 | 用途 |
|
||||
| ---------------- | -------------------------------------------------------------------------------------------------------------- |
|
||||
| `core_rpc_relay` | Body: `{ method, params?, serviceManaged? }` → 转发到本地 **`openhuman-core`** HTTP JSON-RPC (`core_rpc.rs`) |
|
||||
|
||||
从前端使用 **`app/src/services/coreRpcClient.ts`** (`callCoreRpc`)。
|
||||
|
||||
### 窗口管理
|
||||
|
||||
来自 **`commands/window.rs`**(名称可能略有不同;见 `lib.rs`):
|
||||
|
||||
| 命令 | 用途 |
|
||||
| ------------------- | ----------------- |
|
||||
| `show_window` | 显示主窗口 |
|
||||
| `hide_window` | 隐藏主窗口 |
|
||||
| `toggle_window` | 切换可见性 |
|
||||
| `is_window_visible` | 查询可见性 |
|
||||
| `minimize_window` | 最小化 |
|
||||
| `maximize_window` | 最大化 |
|
||||
| `close_window` | 关闭 |
|
||||
| `set_window_title` | 设置标题字符串 |
|
||||
|
||||
### OpenHuman daemon / 服务辅助函数
|
||||
|
||||
来自 **`commands/openhuman.rs`**(见源码获取精确 payload):
|
||||
|
||||
| 命令 | 用途 |
|
||||
| ---------------------------------- | ---------------------------------------------- |
|
||||
| `openhuman_get_daemon_host_config` | 读取 daemon 宿主偏好设置(例如托盘) |
|
||||
| `openhuman_set_daemon_host_config` | 持久化 daemon 宿主偏好设置 |
|
||||
| `openhuman_service_install` | 安装后台服务(平台特定) |
|
||||
| `openhuman_service_start` | 启动服务 |
|
||||
| `openhuman_service_stop` | 停止服务 |
|
||||
| `openhuman_service_status` | 查询状态 |
|
||||
| `openhuman_service_uninstall` | 卸载服务 |
|
||||
|
||||
### 屏幕共享选择器(CEF / macOS)
|
||||
|
||||
来自 **`screen_capture/mod.rs`**。支持 `webview_accounts/runtime.js` 中的页面内 `getDisplayMedia` shim。会话门控:shim 必须在成功枚举/缩略图捕获之前用实时用户手势打开会话。见 issue #713(选择器 UX)+ #812(会话门控)。
|
||||
|
||||
| 命令 | 用途 |
|
||||
| --------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
|
||||
| `screen_share_begin_session` | 从账户 webview 打开 30s 会话,在 `navigator.userActivation.isActive` 手势之后。返回 `{ token, sources }`。每个账户限速 10/分钟。 |
|
||||
| `screen_share_thumbnail` | 将单个来源的缩略图捕获为 base64 PNG。需要 live token 和会话颁发的 `id`。仅 macOS;其他平台返回错误。 |
|
||||
| `screen_share_finalize_session` | 关闭会话。由 shim 在 Share 或 Cancel 时调用;使用未知/过期 token 安全调用(no-op)。 |
|
||||
|
||||
### 已移除 / 不存在
|
||||
|
||||
以下命令**不**存在于当前的 `generate_handler!` 列表中:`exchange_token`、`get_auth_state`、`socket_connect`、`start_telegram_login`。认证和 socket 在 **React** 应用和 **核心** 进程中处理,而非通过这些 IPC 名称。
|
||||
|
||||
### 示例:核心 RPC
|
||||
|
||||
```typescript
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
|
||||
const result = await invoke("core_rpc_relay", {
|
||||
request: {
|
||||
method: "your.rpc.method",
|
||||
params: { foo: "bar" },
|
||||
serviceManaged: false,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
_见 `app/src-tauri/src/lib.rs` 获取权威列表。_
|
||||
|
||||
|
||||
## Core bridge & helpers (`app/src-tauri`) {#core-bridge-helpers-app-src-tauri}
|
||||
|
||||
本文档替代了旧的 "SessionService / SocketService" 拆分。Tauri crate **不**嵌入重复的 Socket.io 服务器或 Telegram 客户端;相反,它专注于对 **`openhuman-core`** 二进制文件的**进程管理**和 **HTTP JSON-RPC**。
|
||||
|
||||
### `CoreProcessHandle` (`core_process.rs`)
|
||||
|
||||
- 解析 **`openhuman-core`** 可执行文件(staging 在 `binaries/` 下或 `PATH` / 开发布局中)。
|
||||
- 启动或附加到核心进程并暴露其 RPC URL (`OPENHUMAN_CORE_RPC_URL`)。
|
||||
- 在 `lib.rs` 的应用设置期间使用 (`app.manage(core_handle)`)。
|
||||
|
||||
### `core_rpc` (`core_rpc.rs`)
|
||||
|
||||
- 核心 JSON-RPC 表面的 HTTP 客户端(localhost)。
|
||||
- 由 **`core_rpc_relay`** 使用,以转发前端的 `method` + `params`。
|
||||
|
||||
### `commands/core_relay.rs`
|
||||
|
||||
- **`core_rpc_relay`**。确保核心正在运行(进程内句柄或**服务管理**路径),然后调用 `core_rpc`。
|
||||
- **`ensure_service_managed_core_running`**。当 RPC 不可用时引导 systemd/launchd 风格服务(核心 CLI 内的平台特定行为)。
|
||||
|
||||
### `commands/openhuman.rs`
|
||||
|
||||
- Daemon 宿主 JSON 配置(例如托盘可见性),位于应用数据目录下。
|
||||
- 为 **openhuman** 后台服务提供 install/start/stop/status/uninstall 辅助函数。
|
||||
|
||||
### `utils/dev_paths.rs`
|
||||
|
||||
- 解析 AI preview 的开发和捆绑资源路径下的 **`src/openhuman/agent/prompts`**。
|
||||
|
||||
### `utils/tauriSocket.ts`(前端)
|
||||
|
||||
不在 `src-tauri` 中,但与 shell **配对**:React 应用监听镜像 Rust 端客户端 socket 活动的 Tauri 事件。见 `app/src/utils/tauriSocket.ts` 和 [前端服务](frontend.zh-CN.md#services-layer) 章节。
|
||||
|
||||
---
|
||||
@@ -0,0 +1,172 @@
|
||||
---
|
||||
description: >-
|
||||
为什么 OpenHuman 自带 Chromium 运行时,我们今天用它做什么,以及同样的 CDP 表面接下来能解锁什么。
|
||||
icon: chrome
|
||||
---
|
||||
|
||||
# Chromium Embedded Framework
|
||||
|
||||
OpenHuman 不运行在平台内置的 webview 上。它通过 `tauri-runtime` 的一个 fork 自带 **Chromium Embedded Framework (CEF) 运行时**,而这一个决策对产品几乎所有 "OpenHuman 知道你的工具里发生了什么" 的功能都是 load-bearing 的。
|
||||
|
||||
本页解释为什么 CEF 在 bundle 中,代码库今天用它做什么,以及同样的表面可以去哪里。
|
||||
|
||||
## 为什么用 CEF 而不是 stock webview
|
||||
|
||||
Stock Tauri 使用每个平台的原生 webview。macOS 上的 WKWebView、Windows 上的 WebView2、Linux 上的 WebKitGTK。这些用于渲染 OpenHuman 应用本身都能正常工作。它们对我们的用例有一个致命的局限性:**没有一个暴露 Chrome DevTools Protocol (CDP)**。
|
||||
|
||||
CDP 是 load-bearing 的原语。OpenHuman 中每个 "观察 Slack / WhatsApp / Telegram / Discord / Meet 内部发生了什么" 的功能都通过 CDP 与这些嵌入应用对话,而非通过注入的 JavaScript。CDP 提供:
|
||||
|
||||
* `Target.getTargets` 用于发现每个页面和服务 worker。
|
||||
* `IndexedDB.requestDatabaseNames` / `requestDatabase` / `requestData` 用于遍历第三方应用的本地存储。
|
||||
* `DOMSnapshot.captureSnapshot` 用于不会触发框架反应性的只读 DOM 检查。
|
||||
* `Runtime.evaluate` 用于短暂的一次性读取(单个固定的 JSON 序列化器,从来不是持久桥接)。
|
||||
* `Page.addScriptToEvaluateOnNewDocument` 用于极少数我们真正需要在页面 JS 运行前渲染器端 shim 的情况。
|
||||
|
||||
Stock webview 不能给我们任何这些。所以我们 vendor CEF。
|
||||
|
||||
Vendored 运行时位于 [`app/src-tauri/vendor/tauri-cef/`](https://github.com/tinyhumansai/openhuman/tree/main/app/src-tauri/vendor/tauri-cef)(从上游 `tauri-cef` 分支 fork 到 `tinyhumansai/tauri-cef:feat/cef-notification-intercept`,当前 CEF 146.4.1)。每个 Tauri crate 在 `app/src-tauri/Cargo.toml` 中通过 `[patch.crates-io]` 指向此 fork。Vendored `cargo-tauri` CLI 将 Chromium 正确捆绑到 `Contents/Frameworks/`;stock `@tauri-apps/cli` 会产生一个损坏的 bundle,在 `cef::library_loader::LibraryLoader::new` 中 panic。[`scripts/ensure-tauri-cli.sh`](../../scripts/ensure-tauri-cli.sh) 在 fork 比安装的二进制文件更新时重新安装 vendored CLI。
|
||||
|
||||
## CEF 今天用于什么
|
||||
|
||||
### 嵌入的第三方 webview
|
||||
|
||||
每个作为托管 Web 应用运行的已连接提供商都有自己的子 CEF webview:
|
||||
|
||||
* WhatsApp Web
|
||||
* Telegram Web
|
||||
* Slack
|
||||
* Discord
|
||||
* Google Meet
|
||||
* LinkedIn
|
||||
* Gmail
|
||||
* Zoom
|
||||
* browserscan
|
||||
|
||||
每个账户的存储隔离到 `{app_local_data_dir}/webview_accounts/{id}/`。两个 Slack workspace,两个浏览器配置文件。代码:[`app/src-tauri/src/webview_accounts/mod.rs`](../../app/src-tauri/src/webview_accounts/mod.rs)。
|
||||
|
||||
### CDP 驱动的扫描器
|
||||
|
||||
每个提供商在 [`app/src-tauri/src/`](https://github.com/tinyhumansai/openhuman/tree/main/app/src-tauri/src) 中都有一个**扫描器模块**。每个扫描器持有到 CEF 的 `--remote-debugging-port=19222` 的长期 WebSocket,并按固定节奏 tick:
|
||||
|
||||
| 扫描器 | 节奏 | 做什么 |
|
||||
| ------------------ | ------------------------------- | -------------------------------------------------------------------- |
|
||||
| `whatsapp_scanner` | 2s DOM tick + 30s 完整 IDB 遍历 | 读取消息存储、拉取媒体元数据 |
|
||||
| `telegram_scanner` | 相同 | 额外加上 QR 登录 hand-off 到原生 Telegram Desktop |
|
||||
| `slack_scanner` | 30s IDB 遍历 | 纯 IDB —— 无需 DOM 抓取 |
|
||||
| `discord_scanner` | 定期 | 通过 CDP 的频道 + DM 状态 |
|
||||
| `meet_scanner` | 定期 | 通话期间的实时字幕 + 参与者状态 |
|
||||
| `imessage_scanner` | 定期 | **无 webview。** 在 macOS 上直接读取 `~/Library/Messages/chat.db` |
|
||||
|
||||
每次扫描都会发出 `webview:event` payload,并直接向核心 RPC POST `openhuman.memory_doc_ingest`,因此无论 UI 窗口是否打开或后台运行,记忆都会增长。
|
||||
|
||||
### Google Meet mascot 摄像头
|
||||
|
||||
最炫的 CEF 技巧。Meet Agent 不只是"参加会议",它还**将自己广播为摄像头**。之所以能工作,是因为 CEF 允许我们:
|
||||
|
||||
1. 在任何 Meet 代码运行前通过 `Page.addScriptToEvaluateOnNewDocument` 注入一个微小桥接 (`camera_bridge.js`)。
|
||||
2. 覆盖 `navigator.mediaDevices.getUserMedia`,使其从隐藏的 640×480 canvas 返回 `MediaStream`,而非真实摄像头。
|
||||
3. 在该 canvas 上渲染 mascot SVG,通过 Rust 经 CDP 驱动的 `window.__openhumanSetMood(...)` 交换情绪状态(idle、thinking、talking)。
|
||||
|
||||
还有一个构建时路径,将 mascot SVG 栅格化为 Y4M,并使用 CEF 的原生 `--use-file-for-fake-video-capture` flag,一个完全原生的 fake-camera 来源,完全不使用 JS。
|
||||
|
||||
代码:[`app/src-tauri/src/meet_video/`](https://github.com/tinyhumansai/openhuman/tree/main/app/src-tauri/src/meet_video)。
|
||||
|
||||
### 原生通知拦截
|
||||
|
||||
`feat/cef-notification-intercept` 上的 fork 为 `Notification.permission`、`Notification.requestPermission()` 和 `navigator.permissions.query({name: "notifications"})` 添加了渲染器端 shim。这些现在在每条运行时代码路径上都安装在真正的 `tauri-runtime-cef` 路径中,因此当 Slack 检查它是否可以显示通知时,答案与 CEF 的权限回调已经授予的内容一致。
|
||||
|
||||
这是 `docs/TAURI_CEF_FINDINGS_AND_CHANGES.md` 的大部分内容。这就是 Slack 在一次会话中不再五次询问相同权限的原因。
|
||||
|
||||
## "不注入新 JS" 规则
|
||||
|
||||
规则记录在 [`CLAUDE.md`](../../CLAUDE.md) 中:**迁移的提供商以零注入 JavaScript 加载**。所有抓取都通过扫描器侧的 CDP 原生进行。
|
||||
|
||||
这很重要,因为任何在第三方来源内部运行的宿主控制代码都是攻击面责任。Slack 内部的持久 JS 桥接离失效只有一个 Slack 更新之遥,离通过攻击者控制的 JS 泄露桥接只有一个错误之遥。从渲染器外部的 CDP 严格更好。
|
||||
|
||||
| 提供商 | 已迁移? | 启动时加载什么 |
|
||||
| ----------- | ------------- | -------------------------------- |
|
||||
| WhatsApp | ✅ | 零 JS |
|
||||
| Telegram | ✅ | 零 JS |
|
||||
| Slack | ✅ | 零 JS |
|
||||
| Discord | ✅ | 零 JS |
|
||||
| browserscan | ✅ | 零 JS |
|
||||
| Gmail | grandfathered | 遗留 `runtime.js` 桥接 |
|
||||
| LinkedIn | grandfathered | 遗留 `LINKEDIN_RECIPE_JS` |
|
||||
| Google Meet | grandfathered | 摄像头 + 音频 + 字幕桥接 |
|
||||
|
||||
遗留注入应该缩小,永远不要增长。新提供商直接走 CDP-only 路径。
|
||||
|
||||
## CEF 预热
|
||||
|
||||
一个隐藏的 CEF webview (`cef-prewarm`) 在应用启动时启动浏览器,因此当用户点击时第一个子 webview 立即生成。它在 `cef::shutdown()` 前被拆除以避免退出时的竞争。见 `app/src-tauri/src/lib.rs` 中 prewarm + 关闭生命周期附近的代码。
|
||||
|
||||
## Windows 启动诊断
|
||||
|
||||
CEF 在 onboarding UI 能够从渲染器故障中恢复之前初始化。如果 Windows 用户报告静默退出、永久的 "Connecting..." 转圈,或在第一个交互窗口出现前的 `tauri-runtime-cef` 断言,请在 issue 中询问这些细节:
|
||||
|
||||
* Windows 版本和完整构建号,特别是 Insider 构建。
|
||||
* OpenHuman 版本和安装包类型(`.msi` 或 `.exe`)。
|
||||
* 重试前是否将 `%LOCALAPPDATA%\com.openhuman.app` 移到了一边。
|
||||
* `[startup]`、`[cef-profile]` 和 `[cef-startup]` 的启动日志行。
|
||||
* 任何命名 `tauri-runtime-cef/src/lib.rs` 的 panic 文本。
|
||||
|
||||
对于 Windows Insider 构建,还要确认相同的安装包是否在当前稳定版 Windows 发布上启动。这会将 profile/缓存问题与 CEF 启动中的 OS/运行时兼容性回归分开。
|
||||
|
||||
## Linux shell fallback(CEF 启动崩溃时)
|
||||
|
||||
在某些 Linux 桌面上,特别是 NVIDIA 专有驱动设置下的 Wayland/XWayland,Tauri/CEF shell 可能在 React 应用变得可用之前的原生窗口配置期间失败。一个已知症状是 CEF 报告主浏览器上下文后的 X11 `BadWindow` 错误。
|
||||
|
||||
当核心本身健康时,你可以通过分别运行核心和前端来继续开发:
|
||||
|
||||
```bash
|
||||
cargo build --bin openhuman-core
|
||||
./target/debug/openhuman-core run --port 7788
|
||||
```
|
||||
|
||||
在另一个终端:
|
||||
|
||||
```bash
|
||||
cd app
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
在常规浏览器中打开 Vite URL,选择 **Advanced** / remote core 模式,将 RPC URL 设置为 `http://127.0.0.1:7788/rpc`,并使用核心写入的 bearer token。这会绕过原生专属功能,如托盘、自动更新和嵌入提供商 webview,但保持智能体、记忆、技能和 RPC 表面可用于调试。
|
||||
|
||||
## 插件审计
|
||||
|
||||
添加到 `app/src-tauri/src/lib.rs` 的任何新内容都必须审计 `js_init_script` 调用。`tauri-plugin-opener` 默认附带一个 init 脚本 (`init-iife.js`),添加了一个全局点击监听器;我们将其配置为 `.open_js_links_on_click(false)`,使其不在第三方 webview 内运行。`tauri-plugin-notification` 的 init 脚本同样从 vendored 副本中删除。
|
||||
|
||||
## 这里可以如何演进
|
||||
|
||||
CDP 表面是通用的。今天它为固定列表的提供商提供记忆摄入;同样的原语可以做更多。
|
||||
|
||||
### 浏览器自动化作为一等智能体工具
|
||||
|
||||
今天智能体有[原生工具](../features/native-tools/README.zh-CN.md)用于文件系统、git、网页搜索和网页获取。下一个明显的工具是**"驱动真实浏览器会话"**:登录用户已认证过的 SaaS,填写表单,抓取分页表格,下载导出。
|
||||
|
||||
plumbing 已经存在。`@openhuman/browser_task` 技能可以启动一个专用 CEF webview,通过 CDP 从核心驱动它,并将结果作为工具调用展示。用户现有的每账户配置文件意味着无需重新认证。
|
||||
|
||||
### Headless CEF 用于服务端回放
|
||||
|
||||
同样的扫描器模式(长期 WebSocket → IDB 遍历 + DOM snapshot)无需 UI 即可工作。核心 sidecar 中的 Headless CEF 可以按计划回放会话,适用于在云端托管核心并希望从不暴露干净 OAuth API 的来源自动获取的用户。
|
||||
|
||||
### 浏览器进程层的隐私 hook
|
||||
|
||||
CEF 的 `CefRequestHandler` 已经允许我们拦截网络请求。从"拦截并记录"到"拦截并重写"只有一小步:广告拦截、跟踪器拦截、每个提供商的 DNS 固定、请求重写。隐私作为一等浏览器功能,而非每个来源内泄漏的 JS shim。
|
||||
|
||||
### CDP 驱动的测试框架
|
||||
|
||||
扫描器模式、生成 webview、遍历 IDB、snapshot DOM、评估一个短暂表达式,在结构上与 E2E 测试编排相同。我们可以将 `@openhuman/web_test` 作为公共技能发布:`connect_cef → snapshot → evaluate → assert`。用纯 Rust 针对任何 Web 应用编写的测试,无需 Selenium / Playwright 依赖。
|
||||
|
||||
### 渲染器 ↔ Rust 消息通道
|
||||
|
||||
今天每个 CDP `Runtime.evaluate` 都是 fire-and-forget。从渲染器到 Rust 的长期双向通道(Tauri 为主机应用做 IPC 的方式)将解锁流式用例:实时打字检测、实时选择/高亮跟踪、主动推送。设计它时不违反"第三方来源中不允许持久 JS 桥接"规则是有趣的约束。
|
||||
|
||||
### 多账户合并
|
||||
|
||||
每个连接账户都有自己的配置文件和自己的 IDB。CDP 可以 snapshot 一个账户的 IDB,与另一个账户的解密合并,并 upsert 到共享的记忆文档中,例如跨三个 workspace 的统一 Slack 记忆。
|
||||
|
||||
## 另请参阅
|
||||
|
||||
* [`docs/TAURI_CEF_FINDINGS_AND_CHANGES.md`](../../docs/TAURI_CEF_FINDINGS_AND_CHANGES.md)。通知权限深度解析。
|
||||
* [`CLAUDE.md`](../../CLAUDE.md)。权威的"不注入新 JS"规则。
|
||||
@@ -0,0 +1,128 @@
|
||||
---
|
||||
lang: zh-CN
|
||||
---
|
||||
|
||||
# Polymarket 集成(读取 + 交易)
|
||||
|
||||
本文档描述 issue #1398 的 Polymarket 集成。
|
||||
|
||||
## 范围
|
||||
|
||||
`polymarket` 工具现在支持以下 API 上的市场浏览和交易工作流:
|
||||
|
||||
- Gamma API (`https://gamma-api.polymarket.com`)
|
||||
- CLOB API (`https://clob.polymarket.com`)
|
||||
|
||||
支持的读取操作:
|
||||
|
||||
- `list_markets`
|
||||
- `get_market`
|
||||
- `list_events`
|
||||
- `get_orderbook`
|
||||
- `get_price`
|
||||
- `get_positions`
|
||||
- `get_balance`
|
||||
- `get_open_orders`
|
||||
- `get_usdc_allowance`
|
||||
|
||||
支持的写入操作:
|
||||
|
||||
- `place_order`
|
||||
- `cancel_order`
|
||||
|
||||
## 架构
|
||||
|
||||
实现位于 `src/openhuman/tools/impl/network/polymarket.rs`,辅助模块包括:
|
||||
|
||||
- `clob_auth.rs`:L1 凭据派生 + L2 HMAC 头
|
||||
- `polymarket_orders.rs`:EIP-712 订单类型数据签名
|
||||
|
||||
关键运行时行为:
|
||||
|
||||
- Layer-2 API 凭据在首次认证调用时派生并缓存。
|
||||
- 派生凭据持久化到 `integrations.polymarket.derived_clob_credentials`(在 secret-store 迁移落地前使用明文配置 fallback)。
|
||||
- 下单前获取 `GET /nonce?user=<eoa>` 以避免重放/nonce 不匹配。
|
||||
- USDC.e 授权通过 Polygon `eth_call` 对 ERC-20 `allowance(owner, spender)` 进行读取。
|
||||
|
||||
## 认证与签名流程
|
||||
|
||||
### L1 握手(一次性引导)
|
||||
|
||||
- 使用 Polygon chain id `137` 签署 CLOB `ClobAuth` EIP-712 payload。
|
||||
- 调用 `POST /auth/api-key`;如需,fallback 到 `GET /auth/derive-api-key`。
|
||||
- 持久化返回的 `{ apiKey, secret, passphrase }` 以供 L2 使用。
|
||||
|
||||
### L2 认证请求
|
||||
|
||||
每个认证的 CLOB 请求签署:
|
||||
|
||||
- `timestamp + method + request_path (+ POST 的 body)`
|
||||
|
||||
Headers:
|
||||
|
||||
- `POLY_ADDRESS`
|
||||
- `POLY_SIGNATURE`
|
||||
- `POLY_TIMESTAMP`
|
||||
- `POLY_NONCE: 0`
|
||||
- `POLY_API_KEY`
|
||||
- `POLY_PASSPHRASE`
|
||||
|
||||
### 订单签名
|
||||
|
||||
`place_order` 使用以下 domain 签署 EIP-712 订单:
|
||||
|
||||
- name: `Polymarket CTF Exchange`
|
||||
- version: `1`
|
||||
- chain id: `137`
|
||||
- verifying contract: `integrations.polymarket.clob_exchange_contract`
|
||||
|
||||
## 权限
|
||||
|
||||
写入操作目前由显式的临时审批 flag 保护。
|
||||
|
||||
- `place_order` 和 `cancel_order` 需要 `approved=true`。
|
||||
- 如果省略或 `false`,工具返回:
|
||||
- `Polymarket write requires explicit user approval. Re-invoke with arguments.approved = true after confirming with the user.`
|
||||
|
||||
这是临时的,直到 #1339 的共享审批门禁集成进来。
|
||||
|
||||
## 配置
|
||||
|
||||
配置路径:`integrations.polymarket`。
|
||||
|
||||
字段:
|
||||
|
||||
- `enabled`(默认 `false`)
|
||||
- `gamma_base_url`(默认 `https://gamma-api.polymarket.com`)
|
||||
- `clob_base_url`(默认 `https://clob.polymarket.com`)
|
||||
- `timeout_secs`(默认 `15`)
|
||||
- `eoa_address`(可选默认用户地址)
|
||||
- `polygon_rpc_url`(默认 `https://polygon-rpc.com`)
|
||||
- `usdc_contract`(默认 `0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174`)
|
||||
- `clob_exchange_contract`(默认 `0x4bFb41d5B3570DeFd03C39a9A4D8dE6Bd8B8982E`)
|
||||
- `derived_clob_credentials`(可选缓存的 L2 凭据)
|
||||
|
||||
## USDC Allowance 合约
|
||||
|
||||
`get_usdc_allowance` 仅报告授权状态;不改变链上状态。
|
||||
|
||||
- Token:Polygon 上的 USDC.e (`0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174`)
|
||||
- Spender:Polymarket exchange (`0x4bFb41d5B3570DeFd03C39a9A4D8dE6Bd8B8982E`)
|
||||
|
||||
如果授权不足,必须单独执行审批(wallet 工具 / 显式用户审批流程)。
|
||||
|
||||
## 错误与重试行为
|
||||
|
||||
- 4xx 错误视为客户端错误,不重试。
|
||||
- 429 和 5xx 错误视为瞬态错误,最多重试 3 次。
|
||||
- 退避固定为每次重试间隔 500ms。
|
||||
- 超时表现为显式的 deadline 错误。
|
||||
|
||||
## 测试策略
|
||||
|
||||
单元测试位于 `src/openhuman/tools/impl/network/polymarket_tests.rs` 及辅助模块测试中。
|
||||
|
||||
- 现有读取路径和重试行为测试保持覆盖。
|
||||
- 新增认证读取操作、写入审批门禁和 Polygon 授权读取的覆盖。
|
||||
- `clob_auth.rs` 测试覆盖 HMAC/头 fixture 行为。
|
||||
- `polymarket_orders.rs` 测试覆盖 domain 和确定性签名 fixture 行为。
|
||||
@@ -0,0 +1,87 @@
|
||||
---
|
||||
description: 将 OpenHuman Core 作为只读 stdio Model Context Protocol 服务器运行。
|
||||
icon: plug
|
||||
lang: zh-CN
|
||||
---
|
||||
|
||||
# MCP 服务器
|
||||
|
||||
OpenHuman Core 可以作为可选的 stdio MCP 服务器运行,供 Claude Desktop、Cursor 或 Zed 等本地 MCP 客户端使用。
|
||||
|
||||
```bash
|
||||
openhuman-core mcp
|
||||
```
|
||||
|
||||
该命令不会启动 HTTP JSON-RPC 服务器。它从 stdin 读取换行分隔的 JSON-RPC 2.0 消息,并将 MCP 响应写入 stdout。日志输出到 stderr;添加 `--verbose` 以获得调试输出。
|
||||
|
||||
## 客户端来源
|
||||
|
||||
在 `initialize` 期间,MCP 服务器捕获 stdio 会话的 `params.clientInfo.name`。名称通过以下方式规范化:修剪首尾空白,转换为小写,将每个非 ASCII 字母数字字符序列替换为单个连字符,然后修剪首尾连字符。例如,`Claude Desktop` 变为 `claude-desktop`,`Cursor` 变为 `cursor`,`Windsurf` 变为 `windsurf`。
|
||||
|
||||
如果客户端省略了 `clientInfo.name`、发送空值,或发送一个规范化后结果为空的名称,会话会回退到裸的 `mcp` 来源标签。可写的 MCP 工具应使用此会话来源标签作为记忆来源,以便旧客户端保持现有的 `mcp` 行为,而可识别客户端可以作为 `mcp:<client>` 写入。
|
||||
|
||||
## 工具
|
||||
|
||||
MCP 表面经过精心设计为只读,并通过现有的控制器注册表以及核心安全策略的读取门禁:
|
||||
|
||||
| MCP 工具 | 背后的 RPC | 用途 |
|
||||
| --- | --- | --- |
|
||||
| `searxng_search`* | `openhuman.tools_searxng_search` | 搜索配置的自托管 SearXNG 实例。 |
|
||||
| `memory.search` | `openhuman.memory_tree_search` | 对记忆树块进行关键词搜索。 |
|
||||
| `memory.recall` | `openhuman.memory_tree_recall` | 对记忆树摘要/块进行语义召回。 |
|
||||
| `tree.read_chunk` | `openhuman.memory_tree_get_chunk` | 读取搜索或召回返回的一个块。 |
|
||||
| `tree.browse` | `openhuman.memory_tree_list_chunks` | 分页块列表,支持来源/实体/时间过滤。 |
|
||||
| `tree.top_entities` | `openhuman.memory_tree_top_entities` | 引用最多的规范化实体,可选按类型过滤。 |
|
||||
| `tree.list_sources` | `openhuman.memory_tree_list_sources` | 不同的摄入来源及其块计数和最后活动时间戳。 |
|
||||
|
||||
* 仅在启用 SearXNG 时存在 `searxng_search`。
|
||||
|
||||
`searxng_search` 在启用 SearXNG 时加入 MCP 目录。它接受 `query`、可选的 `categories`(`web`、`news`、`images`)、可选的 `language`,以及可选的 `max_results`(1-50)。
|
||||
`memory.search` 和 `memory.recall` 接受 `query` 加可选的 `k`(默认 10,上限 50)。`tree.read_chunk` 接受 `chunk_id`。`tree.browse` 接受可选的 `source_kinds`、`source_ids`、`entity_ids`、`since_ms`、`until_ms`、`query`、`k` 和 `offset`。`tree.top_entities` 接受可选的 `kind` 和 `k`。`tree.list_sources` 接受可选的 `user_email_hint`。
|
||||
|
||||
在 `config.toml` 或通过环境变量启用 SearXNG:
|
||||
|
||||
```toml
|
||||
[searxng]
|
||||
enabled = true
|
||||
base_url = "http://localhost:8080"
|
||||
max_results = 10
|
||||
default_language = "en"
|
||||
timeout_seconds = 10
|
||||
```
|
||||
|
||||
```bash
|
||||
OPENHUMAN_SEARXNG_ENABLED=true
|
||||
OPENHUMAN_SEARXNG_BASE_URL=http://localhost:8080
|
||||
OPENHUMAN_SEARXNG_MAX_RESULTS=10
|
||||
OPENHUMAN_SEARXNG_DEFAULT_LANGUAGE=en
|
||||
OPENHUMAN_SEARXNG_TIMEOUT_SECONDS=10
|
||||
```
|
||||
|
||||
## 工具注册表
|
||||
|
||||
HTTP JSON-RPC 服务器还暴露一个只读的全局工具注册表,供需要发现元数据而不打开 MCP stdio 会话的智能体和仪表板使用:
|
||||
|
||||
| RPC 方法 | 用途 |
|
||||
| --- | --- |
|
||||
| `openhuman.tool_registry_list` | 列出 MCP stdio 工具和控制器支持的工具,包含稳定的 `tool_id`、路由、版本、输入/输出 schema、允许的智能体、标签、启用状态和健康状况。 |
|
||||
| `openhuman.tool_registry_get` | 通过 `tool_id` 返回一个注册表条目,例如 `memory.search` 或 `tools.web_search`。 |
|
||||
|
||||
注册表仅用于发现。它不改变工具分派或权限检查;MCP 调用仍通过 `tools/call`,控制器支持的工具仍通过其现有的 JSON-RPC 方法路由。
|
||||
|
||||
## 冒烟测试
|
||||
|
||||
```bash
|
||||
printf '%s\n' \
|
||||
'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"smoke","version":"0"}}}' \
|
||||
'{"jsonrpc":"2.0","method":"notifications/initialized"}' \
|
||||
'{"jsonrpc":"2.0","id":2,"method":"tools/list"}' \
|
||||
| openhuman-core mcp
|
||||
```
|
||||
|
||||
响应应包含来自 `initialize` 的 `capabilities.tools` 和来自 `tools/list` 的精选工具名称。成功的运行向 stdout 写入恰好两行紧凑的 JSON 响应;`notifications/initialized` 消息是通知,没有响应。
|
||||
|
||||
```json
|
||||
{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-06-18","capabilities":{"tools":{}},"serverInfo":{"name":"openhuman-core","version":"<crate version>"},"instructions":"..."}}
|
||||
{"jsonrpc":"2.0","id":2,"result":{"tools":[{"name":"memory.search",...},{"name":"memory.recall",...},{"name":"tree.read_chunk",...},{"name":"tree.browse",...},{"name":"tree.top_entities",...},{"name":"tree.list_sources",...}]}}
|
||||
```
|
||||
@@ -0,0 +1,81 @@
|
||||
---
|
||||
description: 发布节奏、版本策略、OAuth 与安装包规则。发布是如何运作的。
|
||||
icon: ship
|
||||
lang: zh-CN
|
||||
---
|
||||
|
||||
# 发布策略:最新桌面构建与 OAuth
|
||||
|
||||
本 runbook 描述了我们如何避免用户在**过时的桌面安装包**上完成 **OAuth**(包括 **Gmail**),而规范流程始终要求**最新**发布版本。
|
||||
|
||||
## 分发
|
||||
|
||||
- [tinyhumansai/openhuman](https://github.com/tinyhumansai/openhuman/releases) 的 **GitHub Releases** 是桌面构建的主要来源。
|
||||
- **Tauri 更新器**端点(见 `scripts/prepareTauriConfig.js` 和发布工作流)应将用户指向当前发布产物。
|
||||
- **淘汰旧稳定版产物:** 当弃用一条发布线时,在 **GitHub Releases** 上移除或隐藏过时的安装包资源,将 **网站 / CDN** 下载链接更新为 **releases/latest**(或当前版本),刷新**更新器 manifest**(例如 Gist / `latest.json`)使其不再指向已弃用的构建,并抽查旧直接 URL 在适当位置是否被**重定向、返回 404 或 410**。验证方式:尝试从文档或书签中已知的旧资源 URL,确认它们不再提供主要安装路径。
|
||||
|
||||
## OAuth 最低应用版本
|
||||
|
||||
生产 Web 构建在**构建时**嵌入一个**最低支持的应用 semver**,使 OAuth 深度链接无法在已弃用的二进制文件上完成。每个安装包携带构建时设定的 floor;对于从不升级的用户,提高 floor 需要他们安装一个**新**的发布版本(或通过应用内更新)。可选的未来工作:仅通过**运行时** API 强制执行移动的最低版本,捆绑值仅作为 fallback。
|
||||
|
||||
| 变量 | 用途 |
|
||||
| ------------------------------------ | --------------------------------------------------------------------------------------------------------------------- |
|
||||
| `VITE_MINIMUM_SUPPORTED_APP_VERSION` | 例如 `0.51.0` —— 桌面应用必须 **≥** 此版本才能完成 `openhuman://oauth/success`。 |
|
||||
| `VITE_LATEST_APP_DOWNLOAD_URL` | 可选;默认为 `https://github.com/tinyhumansai/openhuman/releases/latest`。当门禁阻止 OAuth 时打开。 |
|
||||
|
||||
将这些配置为 **GitHub Actions 变量**。它们必须同时存在于独立的 **`pnpm build`** 步骤和 **`.github/workflows/build-desktop.yml`** 中的 **`tauri-apps/tauri-action`** 步骤环境变量中(由 `release-production.yml` / `release-staging.yml` 调用的可重用矩阵)以及 `build-windows.yml`,以便嵌入已发布安装包的 Vite bundle 包含该门禁。本地开发时保持 `VITE_MINIMUM_SUPPORTED_APP_VERSION` **未设置**(门禁禁用)。
|
||||
|
||||
实现:`app/src/utils/oauthAppVersionGate.ts`、`app/src/utils/desktopDeepLinkListener.ts`。
|
||||
|
||||
## Gmail / Google Cloud OAuth
|
||||
|
||||
- Google Cloud Console 中的 **Redirect URIs** 必须匹配**当前**后端 + 隧道回调路径。
|
||||
- 桌面 scheme(`openhuman://`)是稳定的;当 `VITE_MINIMUM_SUPPORTED_APP_VERSION` 设置时,**已安装的二进制文件**必须满足最低版本。
|
||||
|
||||
## 发布清单(避免回归)
|
||||
|
||||
1. 按照现有版本工作流提升 `app/package.json` 和 `app/src-tauri/tauri.conf.json`(以及根目录 `Cargo.toml` / core)的版本。
|
||||
2. 当弃用对旧安装包的支持时,在该发布**之前**或**同时**将 **`VITE_MINIMUM_SUPPORTED_APP_VERSION`** 设置为新的 floor(仓库 Actions 变量 + 上述两个工作流步骤)。
|
||||
3. 从用户可见表面(GitHub Release 资源、网站、CDN、更新器 feed)移除、重定向或淘汰旧稳定版安装包和陈旧**更新器**条目。确认已弃用的资源无法从默认安装/更新流程中访问。
|
||||
4. 从 **releases/latest** 的全新安装上冒烟测试 **Gmail 连接**。
|
||||
5. 完成[手动冒烟清单](../../docs/RELEASE-MANUAL-SMOKE.md),然后将完成的签字块(逐字复制,每个已勾选项目保持勾选)粘贴到发布 PR 描述中,然后再打 tag。
|
||||
|
||||
## 工作流:staging vs. production
|
||||
|
||||
两个一等 GitHub Actions 工作流,每个环境一个。按意图选择,而非切换 flag。
|
||||
|
||||
| 工作流 | 分支 | 提升 | 推送的 Tags | 并发组 | 使用场景 |
|
||||
| ------------------------------------------------------- | --------- | ------- | -------------------------- | ----------------------- | --------------------------------------------------------------------- |
|
||||
| [`release-staging.yml`](../../.github/workflows/release-staging.yml) | `main` | 仅 `patch` | `v<version>-staging` | `release-staging` | 为 QA 切割 staging 构建。运行频繁;semver 移动范围窄。 |
|
||||
| [`release-production.yml`](../../.github/workflows/release-production.yml) | `main` | `patch` / `minor` / `major`(仅在 `main_head` 上) | `v<version>` | `release-production` | 提升已验证的 staging tag,或从 `main` HEAD 热修。 |
|
||||
|
||||
两个流程使用的矩阵构建 / 签名 / Sentry-DIF / 产物上传流水线位于 [`.github/workflows/build-desktop.yml`](../../.github/workflows/build-desktop.yml) 中,作为 `workflow_call` 可重用工作流。上述两个顶层工作流拥有 ref 解析、版本提升、tagging 和发布/清理;构建本身是共享的。
|
||||
|
||||
### 切割 staging 构建
|
||||
|
||||
1. 通过 `workflow_dispatch` 从 `main` 运行 **Release (Staging)**。
|
||||
2. 工作流在 `main` 上提升 `patch`,commit `chore(staging): vX.Y.Z`,推送分支,并在该 commit 上创建不可变的 `vX.Y.Z-staging` tag。
|
||||
3. 构建矩阵从 **tag**(而非 main HEAD)运行,因此即使 `main` 已经前进,rerun 也会重建字节相同的内容。
|
||||
4. 失败时 staging tag 会被自动删除;`main` 上的提升 commit 保留,因此下一次切割从 `vX.Y.(Z+1)` 继续。
|
||||
|
||||
没有单独的 `staging` 分支,staging 切割和 production 提升都存在于 `main` 上。两者仅通过 tag 后缀(`-staging` vs 无)和创建工作流来区分。
|
||||
|
||||
### 提升为 production(默认流程)
|
||||
|
||||
1. 通过 `workflow_dispatch` 以 `release_source = staging_tag`(默认)运行 **Release Production**。
|
||||
2. 留空 `staging_tag` 以提升最新的 `v*-staging`,或传入显式 tag(例如 `v1.2.4-staging`)以固定版本。
|
||||
3. 工作流去除 `-staging` 后缀,在同一 commit 上创建 `v<version>`,并从该 tag 运行 production 构建矩阵。**不再提升版本**,产物复用 staging 已验证的内容。
|
||||
|
||||
### 从 `main` HEAD 热修
|
||||
|
||||
1. 通过 `workflow_dispatch` 以 `release_source = main_head` 和所需的 `release_type`(`patch` / `minor` / `major`)运行 **Release Production**。
|
||||
2. 工作流运行遗留的提升-and-tag 路径:在 `main` 上提升,commit `chore(release): vX.Y.Z`,推送,tag `vX.Y.Z`,构建。
|
||||
3. 仅当需要不经过 staging 的 production-only 修复时才使用此路径。
|
||||
|
||||
### Tag 策略与回滚
|
||||
|
||||
- **命名。** Staging tag 使用 SemVer 预发布后缀 `-staging`(`v1.2.4-staging`),因此它们在排序上位于匹配的 production tag *之前*。提升到 production 时逐字去除后缀;两个 tag 之间捆绑安装包中嵌入的版本是相同的。
|
||||
- **冲突。** 如果目标 tag 已存在于本地或 `origin` 上,两个工作流都会快速失败。通过删除陈旧 tag(仅限组织维护者)或跳过它来解决。
|
||||
- **回滚(production)。** 失败的构建矩阵会触发 `cleanup-failed-release`,删除草稿 GitHub Release 和 `v<version>` tag。它从中提升的 staging tag 保持不变,修复后可以重新提升。
|
||||
- **回滚(staging)。** 失败的 staging 构建会删除 `v<version>-staging` tag。`main` 上的提升 commit 保留;下一次 staging 切割从新的 patch 号继续,而不是重新使用它(我们接受 patch 号中的一个小"缺口",而不是与并发合并竞争)。
|
||||
- **谁可以删除 tag。** 与 `main` 相同的写入权限。工作流驱动的清理通过工作流的 token 使用 `actions/github-script` 运行删除(GitHub App token 仅由 `prepare-build` 用于提升 commit + tag 推送);手动删除(`git push --delete origin <tag>`)需要同等的维护者权限。
|
||||
Reference in New Issue
Block a user