# OpenWorker 源码分析：一个"能交付成品的 AI 同事"是怎样炼成的

> **分析对象**：openworker（`andrewyng/openworker`），Python 包名 `coworker`，版本 Beta，基于 **commit `db93d75`**（2026-07-25）。
> **代码规模**：Python 202 个 `.py`（后端引擎/连接器/记忆/自动化），前端 151 个 `.ts/.tsx`（React GUI），另有 Rust（Tauri 桌面壳 + STT 语音侧车）。许可 **MIT**。
> **一句话定位**：吴恩达团队开源的**本地优先 AI 同事**——住在你桌面上，交付**成品**（一份文档、一条带数字的 Slack 回复、一张理顺的日历），而不是聊天记录。Provider 层**自研**（原生 OpenAI/Anthropic/Gemini + OpenAI 兼容端点），**aisuite**（吴恩达的统一 LLM 库）仅用于工具层（`ai.tool` / `ai.toolkits`）。
> **读者对象**：已经读过至少一份本系列分析的读者。本文沿用统一的 12 章框架，标注 `文件:行号`，你可以照着索引回源码核对。与八大编程 Agent 相比，OpenWorker 是**另一个物种**：它不为"写代码"而生，而为"像同事一样把日常活儿干完"而生——所以它的重心全在**无人值守、审批治理、连接器、定时**上。

---

> 🗺️ **配套架构图**：本项目在[**七大 Agent 架构图库**](../架构图库.html#ch5)里有一张专门的图——**跨语言进程边界 + 带外审批收件箱**。
> 图库的每张图都先写清「回答什么问题」和「承重墙论点」，并经三轮审阅与渲染验收。

## 目录

1. [项目概览：不是编程助手，是"数字同事"](#ch1)
2. [全景架构：四层 + 三种语言 + 一条事件流](#ch2)
3. [启动流程：三个入口与一个被监管的侧车](#ch3)
4. [输入捕获与分流：桌面、Slack @提及、无人值守](#ch4)
5. [上下文组装：persona + 记忆 + 技能 + 每回合临时上下文](#ch5)
6. [Agent 主循环（心脏）：TurnEngine 的并发、中断与持久恢复](#ch6)
7. [工具系统与权限：aisuite 工具 + 五档模式 + 标准规则](#ch7)
8. [记忆：显式 SQLite 事实，而非自动压缩](#ch8)
9. [子 agent 与多 agent：persona 即 agent + 只读 explorer + self-wake](#ch9)
10. [生态：persona 市场、技能、MCP、40 个连接器、十几家模型](#ch10)
11. [功能特性：收件箱、定时自动化、换模型、PDF/视觉降级、审计](#ch11)
12. [总结：三个最独特的设计与取舍](#ch12)

---

<h2 id="ch1">第 1 章 项目概览：不是编程助手，是"数字同事"</h2>

**OpenWorker 是什么？** 用 README 第一句话说：一个开源的 AI 同事，交付**成品**而非聊天。你告诉它想要的**结果**——"准备一份客户简报""理顺我的日历""看看这个版本在 Jira 和 GitHub 上到哪一步了"——它拆成步骤，跨桌面、文件、已连接的应用干活，在做**有后果的事**（发消息、改日历、跑命令）之前跟你确认，最后给你**做好的东西**。

**谁在做？** 吴恩达（Andrew Ng）团队。README 称"built on [aisuite](https://github.com/andrewyng/aisuite)"（`README.md:107`），但需注意：**aisuite 实际仅用于工具层**（`ai.tool` / `ai.toolkits`——见 `subagent.py` 等约 21 个文件的 import）。**Provider 层是自研的**：`providers/registry.py` 原生实现 OpenAI/Anthropic/Gemini provider，加 `_openai_compat`（用 `OpenAIProvider` 指向各厂商的 OpenAI 兼容端点）；`router.py` 的 `ProviderRouter` 按 `provider:` 前缀路由。aisuite 在 `providers/` 目录里仅在注释中出现且标注为"later swap"。`pyproject.toml` 里 aisuite 被 pin 到具体 commit（`aisuite @ git+…@1b4bbf30`）。

**定位与卖点**（对比八大编程 Agent 尤其鲜明）：

- **本地优先（local-first）**：agent 循环、对话、连接器令牌、模型密钥——全在你机器上的本地密钥库里（`README.md:59-61`）。唯一的云端部件是一个"撮合 OAuth 握手"的小服务；你也可以完全不登录，用手填的凭证跑连接器。
- **模型无关（BYO key）**：不绑任何一家。OpenAI / Anthropic / Google Gemini / Inkling / GLM(Z.ai) / DeepSeek / Kimi / Qwen / MiniMax / Mistral / Grok(xAI)，加 Together/Fireworks 上的开源权重模型，加 **Ollama 全本地**（`README.md:53-55`）。有一张"精选清单"标注哪些被验证过能干工具调用的活。
- **交付成品**：文档、表格、报告、网页都落成你能打开分享的**文件**。
- **从 Slack 就能用**：在频道里 @OpenWorker，桌面上开一个会话，用你的工具干活，答案作为线程回复发回来。
- **能定时跑**：晨间简报、周报、盯一个频道——循环工作用自动化排期，跑完落在应用里带完整逐字记录。
- **动手前先问**：写、发、shell 命令都要审批。无人值守的跑法把"要问的事"停进**收件箱**，而不是自作主张。

> 🧠 **一句话**：八大编程 Agent 争的是"谁更会写代码"；OpenWorker 换了个问题——"怎么让一个模型像同事一样，把跨应用的日常活儿**无人值守地干完**，还不闯祸？"答案的重量全压在**收件箱、审批治理、连接器、定时**这四件事上。

**包名的错位**：仓库叫 openworker，Python 包叫 `coworker`，CLI 命令又叫 `openworker`（`pyproject.toml` 的 `[project.scripts]`）。产品对外是 OpenWorker，代码里到处是 coworker——本文源码引用用 `coworker/…`，产品名用 OpenWorker。

---

<h2 id="ch2">第 2 章 全景架构：四层 + 三种语言 + 一条事件流</h2>

README 自己画了一张分层图（`README.md:32-41`），拆开看是**四层、三种语言**：

```mermaid
flowchart TB
    subgraph L1["① 桌面壳 · Rust/Tauri（surfaces/gui/src-tauri）"]
        SHELL["原生窗口 + 自动更新 + 监管 Python 服务"]
    end
    subgraph L2["② 界面 · React（surfaces/gui/src）"]
        GUI["会话视图 · 工具卡 · 审批卡 · 收件箱 · 设置"]
        STT["Rust STT 侧车（stt/）：语音输入"]
    end
    subgraph L3["③ 本地 agent 服务 · Python（coworker/）"]
        MGR["SessionManager 编排"]
        ENG["TurnEngine 主循环"]
        PERM["权限引擎"]
        INBOX["收件箱（跨会话人类注意力队列）"]
        SCHED["调度器（定时自动化）"]
    end
    subgraph L4["④ 你的东西（全用你的密钥，在你机器上）"]
        FILES["文件 & 终端"]
        CONN["40 个连接器 + MCP"]
        MODEL["任意 provider 的模型"]
    end
    SHELL --> GUI --> MGR
    STT --> GUI
    MGR --> ENG --> PERM
    ENG --> INBOX
    MGR --> SCHED
    ENG -->|工具| FILES
    ENG -->|工具| CONN
    ENG -->|自研 provider 层| MODEL
```

- **① Rust/Tauri 壳**：原生窗口、签名/公证、自动更新，并**把 Python 服务当侧车监管**（`surfaces/gui/src-tauri`）。
- **② React 界面 + Rust STT**：会话、工具卡、审批卡、收件箱、设置都在这层；`stt/`（Rust）是语音输入侧车。
- **③ Python agent 服务**：真正的大脑。`coworker/` 包里，`SessionManager`（`server/manager.py`，3762 行）编排会话，`TurnEngine`（`engine.py`）是主循环，权限引擎、收件箱、调度器围着它转。用 **FastAPI + uvicorn** 起（`server/run.py`）。
- **④ 你的东西**：文件/终端、连接器/MCP、模型——全用你的密钥、在你机器上。

**贯穿全栈的契约：一条事件流。** `TurnEngine` 不返回结果，而是 `yield` 一串 `Event`（`events.py`）：`TURN_START` → `ASSISTANT_DELTA`/`REASONING_DELTA`（流式文字/思考）→ `TOOL_PROPOSED` → `PERMISSION_REQUIRED` → `TOOL_STARTED` → `TOOL_FINISHED` → `ITERATION_END` → `TURN_END`（或 `ERROR`/`INTERRUPTED`）。GUI、TUI、Slack 连接器都是**同一条事件流的消费者**——界面只是把事件画出来。

> 🧠 **一句话**：把界面（Rust/React）和大脑（Python）用一条**事件流**焊死，是它能"同一个会话，桌面看得见、Slack 也答得上、无人值守还在跑"的根。谁来消费事件都行，事件本身不关心。

---

<h2 id="ch3">第 3 章 启动流程：三个入口与一个被监管的侧车</h2>

`pyproject.toml` 声明了三个命令行入口：

| 命令 | 入口 | 干什么 |
|---|---|---|
| `openworker` | `coworker.cli:main` | 启动 **TUI**（Textual 终端界面），默认 `code` 技能（`cli.py:1`） |
| `openworker-server` | `coworker.server.run:main` | 启动 **FastAPI + uvicorn 服务**，桌面 GUI 的侧车也用它（`server/run.py:1`） |
| `openworker-connectors` | `coworker.connectors.cli:main` | 连接器管理 CLI |

**桌面模式下的启动最有意思**：Tauri 壳把 Python 服务当**侧车**拉起，并设 `COWORKER_EXIT_WITH_PARENT=1`——**父进程（GUI）一死，服务立刻自杀**（`server/run.py:_exit_when_orphaned`）。这里有个被踩过的坑：PyInstaller onefile 打包时，服务是 GUI 的**孙进程**（中间隔着 bootloader），`getppid()` 指向 bootloader 而非 GUI，所以不能靠"重新认爹"检测——得让 GUI 把自己的 PID 显式传进 `COWORKER_PARENT_PID`，POSIX 用 `os.kill(pid, 0)` 轮询存活，Windows 阻塞在进程句柄上。**注释里写着这个 bug 曾导致"每次退出 App 都漏一对服务进程"。**

**鉴权**：独立跑的 `openworker-server` 每次启动生成一次性 token 写到 `<state-dir>/sidecar-8765.token`（仅用户可读），API 调用带 `X-OpenWorker-Token` 头；桌面 App 则用**内存里的**启动 token，从不落盘（`README.md:85-88`）。

**统一状态**：TUI、GUI、服务共享**同一个全局库**——`state_dir()/coworker.db`（SQLite 记忆）+ `ConversationStore`（`cli.py` 里 `SQLiteMemoryStore(data_dir/"coworker.db")` + `ConversationStore(data_dir)`）。所以你在终端开的会话、在桌面开的会话，落在同一处。

```mermaid
flowchart LR
    U["用户"] -->|双击 App| TAURI["Tauri 壳"]
    TAURI -->|拉起侧车 + 传 PID| SRV["openworker-server<br/>FastAPI+uvicorn"]
    U -->|终端| TUI["openworker (TUI)"]
    SRV --> STORE["统一全局库<br/>coworker.db + Conversations"]
    TUI --> STORE
    TAURI -. 一死 .-> KILL["孤儿守卫：服务自杀"]
    SRV -. 监视父 PID .-> KILL
```

---

<h2 id="ch4">第 4 章 输入捕获与分流：桌面、Slack @提及、无人值守</h2>

OpenWorker 的"输入"来源比编程 Agent 多得多——不只是你在框里打字，还有 Slack/Telegram 消息、定时触发、自我唤醒。分流的关键是**一个会话由恰好一个 persona 诞生**，之后所有输入都喂给这个会话的 `TurnEngine`。

**几种输入路径**：

- **桌面/TUI 打字**：直接 `engine.run(user_input, source=…)`。`source` 是一个"仅供显示"的旁路（连接器消息带的元数据），随持久化的用户消息走，但**发给模型前会被剥掉**（`engine.py:_outbound_messages`，见第 5 章）。
- **会话中途插话（steering）**：`engine.queue_steering(text)`（`engine.py:150`）——不打断当前回合，等这一批工具跑完/模型说完话后，把插话作为新的用户消息注入（`_inject_steering`，`engine.py:866`）。
- **Slack @提及 → 生成会话（§31）**：这是同事属性的点睛之笔。当 @OpenWorker 在一个**没有订阅会话**的频道被 @ 到，路由器**生成一个"拥有这条线程"的 coworker 会话**并回复进去（`mentions.py`）。妙在 `thread_target` 这个字符串（`"slack:C0123:1700….000100"`）**一串三用**：① 线程→会话的去重查找键；② `send_message` 的投递目标；③ §25 标准授权规则的 target。而且 `get_engine` 每次重建引擎都从这个记录**重推 `permissions.task_rules`**——所以"线程内免问回复"的预授权**能在服务重启后存活**。

```mermaid
flowchart TD
    MENT["Slack 频道里 @OpenWorker"] --> HAS{"该线程已有<br/>订阅会话？"}
    HAS -->|有| ROUTE["路由到已有会话"]
    HAS -->|没有| SPAWN["生成'拥有此线程'的会话"]
    SPAWN --> TARGET["thread_target 一串三用"]
    TARGET --> T1["① 去重查找键"]
    TARGET --> T2["② send_message 投递目标"]
    TARGET --> T3["③ §25 标准授权 target<br/>（重启后从 mentions 重推 task_rules）"]
    ROUTE --> ENG["喂给会话的 TurnEngine"]
    SPAWN --> ENG
```

- **无人值守（Unattended）**：会话可以被设成"无人值守"。这时它要问人的事**不会内联弹窗**，而是进**跨会话收件箱**（见第 6、11 章）。这就是"人不在也能跑，需要人时把 ask 停下来等你"的分流开关。

**有人值守 vs 无人值守**，只差一个 `visibility`：`VIS_INLINE`（内联，在输入框答，重连时重发）vs `VIS_INBOX`（进跨会话队列）——但底层是**同一条持久、可等待、任意界面可回答的记录**（`inbox.py:38-39`）。

---

<h2 id="ch5">第 5 章 上下文组装：persona + 记忆 + 技能 + 每回合临时上下文</h2>

组装全在 `agent.py` 的 `build_engine`（`agent.py:109`）——把一个 persona 拼成一台 `TurnEngine`。

**静态系统提示**按顺序拼（`agent.py:243-266`）：

```
persona.system_prompt
+ 叙述引导（每批工具前写一句"我在干嘛"，作为实时进度给用户看，agent.py:78）
+ environment_context(工作区)
+ AGENTS.md（工作区约定，若有）
+ 记忆指南 + 已记住的事实（若接了记忆库）
+ 技能目录（渐进式披露：先给清单，用 load_skill 按需加载全文）
```

**按家族分装工具**（这是 OpenWorker 取代"按 agent 名 if-else"的设计，`agents/base.py:39`）：

| 家族/标志 | 装什么工具 | 出处 |
|---|---|---|
| `code` 家族 | 只读 **explorer 子 agent**（把宽泛调研外包出去，自己留上下文干正事） | `agent.py:217` |
| `knowledge` 家族 | **定时调度**、**self-wake**、`request_directory`（多目录）、频道订阅 | `agent.py:228,240` |
| `messaging` 标志 | `send_message` + `send_file`（deliverable 交进聊天，**独立审批面**） | `agent.py:169-175` |
| `connectors` 标志 | 25+ 集成工具（按会话有效连接器过滤） | `agent.py:190-204` |
| 所有 agent | web 搜索（无 key 的 DuckDuckGo 默认）+ web fetch + `ask_user` | `agent.py:206-210` |

**每回合临时上下文（ephemeral）**——这是它跨 provider 稳态注入的巧招。中途系统消息在各家 provider 里不可靠，所以动态上下文**挂在最后一条用户消息尾部、只在发送时拼、从不持久化**（`engine.py:_outbound_messages:963-981`）。两个产出者（`agent.py:context_provider:297`）：① plan/discuss 模式提醒（模式能中途翻，所以每回合查而非烤进静态提示）；② 活的目录清单（无主 Cowork 会话能中途拿到新文件夹）。

**发给模型前的"净化"**（`engine.py:_outbound_messages:878`）——`self.messages` 是唯一的 provider 喂料，但要先剥干净：

- 剥掉仅显示的旁路：`source`（连接器卡）、`_display`（隐私过滤计数）、`ts`（时间戳）、`reasoning`（思考文字）；整条 `notice`（错误/中断/换模型标记）直接丢。
- **PDF 附件按当前模型重新适配**：原生支持 PDF 的模型给真文档，其余给本地抽文/转页图的降级（`pdf_support.py`）。
- **图片同理**：无视觉能力的模型收到一个可见占位符 `[image attachment — not viewable by this model]`，而不是它会拒收的负载。
- 关键：这些**每次调用都重判**，所以会话中途换模型（第 11 章）永远做对的事。

> 🧠 **一句话**：历史是"规范 OpenAI 形状"，能力适配放在**发送那一刻**、按**当前模型**做——这条设计让"随时换模型""附件随模型能力升降级"变成免费的副产品，而不是特例代码。

---

<h2 id="ch6">第 6 章 Agent 主循环（心脏）：TurnEngine 的并发、中断与持久恢复</h2>

`TurnEngine`（`engine.py:52`）是整个项目最见功力的地方。它是**异步**循环，但把阻塞的 provider/工具调用用 `asyncio.to_thread` 包起来，好让循环（和消费事件的界面）始终不卡。一个用户回合跨很多次"模型↔工具"迭代，直到模型不再要工具、撞到闸门、或被中断。默认迭代上限 **150**（`config.py:31`；引擎 dataclass 默认 12，explorer 子 agent 10）。

```mermaid
flowchart TD
    START["run(user_input)"] --> APPEND["追加用户消息 + TURN_START"]
    APPEND --> LOOP{"未达迭代上限?"}
    LOOP -->|否, 已到上限| MAXOUT["TURN_END: max_iterations"]
    LOOP -->|是| STREAM["_astream: 线程+队列桥接<br/>流式吐 ASSISTANT_DELTA/REASONING_DELTA"]
    STREAM --> TOOLS{"模型要工具?"}
    TOOLS -->|否| STEER{"有插话?"}
    STEER -->|有| INJECT["注入 steering, 继续"] --> LOOP
    STEER -->|没有| DONE["TURN_END: completed"]
    TOOLS -->|要| AUTH["逐个鉴权（串行，审批是交互的）"]
    AUTH --> SPLIT{"按风险分流"}
    SPLIT -->|low risk 只读/搜索| PAR["asyncio.gather 并发跑"]
    SPLIT -->|写/shell/未标注| SER["严格按序一个个跑"]
    PAR --> REC["记录结果 + TOOL_FINISHED"]
    SER --> REC
    REC --> CANCEL{"被中断?"}
    CANCEL -->|是| INT["每个待跑工具补一个错误结果<br/>INTERRUPTED（不留孤儿）"]
    CANCEL -->|否| LOOP
```

**四个硬核细节**：

1. **并发低危 / 串行高危**（`engine.py:_handle_tool_calls:437`）：一个回合里模型要了好几个工具，**只读/搜索（metadata 标 `risk_level="low"` 且 `requires_approval=False`）并发跑**（`asyncio.gather`），写/shell/任何没标注的**严格按调用序一个个跑**（`_parallel_safe:517`）。

2. **任意状态可中断，且不留孤儿**（`engine.py:request_interrupt:120`）：Stop 能从**任何状态**停——流式中（生产线程在两个 chunk 之间丢流）、工具中（interrupt hook 杀掉正在跑的 shell 命令）、等审批/提问/计划中（await 解析为"被中断"）、迭代间（循环检查点）。**每个还没答的 `tool_call` 都会补一条工具错误结果**——因为托管的 chat 模板会拒收孤儿 tool_call，而持久恢复会把它重新弹一遍。

3. **流式桥接**（`engine.py:_astream:388`）：provider 的**阻塞**流生成器跑在线程里，通过 `loop.call_soon_threadsafe` 往 `asyncio.Queue` 塞 chunk；主循环把队列**和 Stop 事件赛跑**，所以一个卡住的流（首 token 前的干等、僵死连接）拖不住整个回合。

4. **持久恢复（durable resume）**（`engine.py:resume:250`）——**这是"同事"的地基**：一个在提示处挂起并被持久化的回合，在**重启/引擎被驱逐**之后能续上。做法：重新处理最后一条 assistant 消息里**还没被回答的 tool_call**（`_unanswered_trailing_tool_calls:268`）——那些提示回调会找到**已解决的收件箱项**直接返回、不再弹；已回答的调用被跳过，所以什么都不会双跑。再跑模型循环把回合收尾。

另外还有 `retry()`（provider 报错后重跑，守在"尾部是错误 notice"上，`engine.py:237`）和 `switch_model()`（中途换模型，第 11 章）。

> 🧠 **一句话**：这台引擎把"中断"和"恢复"当**一等公民**——任何时刻能停、停下不留孤儿、重启还能从收件箱那条"待答"续上。没有这三样，"无人值守的 AI 同事"只是句口号。

---

<h2 id="ch7">第 7 章 工具系统与权限：aisuite 工具 + 五档模式 + 标准规则</h2>

**工具的底子是 aisuite。** `tools/subagent.py` 里能直接看到：`import aisuite as ai`，工具用 `ai.tool(fn, metadata=ai.ToolMetadata(category=…, risk_level=…, requires_approval=…))` 包，工具箱用 `ai.toolkits.files(root=ws)` / `ai.toolkits.git(root=ws)`（`subagent.py:60-64,129-137`）。OpenWorker 在 aisuite 工具之上叠自己的实现（窗口化 `read_file`、自己的 `grep` 等），并挂上 `ToolMetadata`——**风险等级就是从这里来的**，主循环的"并发/串行"和权限引擎的判决都读它。

**权限引擎五档模式**（`permissions.py:37`）：

| 模式 | 含义 |
|---|---|
| `discuss` | 只读对话：不改、不走计划流程 |
| `plan` | 只读 + 计划契约（探索 → `propose_plan` → 执行） |
| `interactive` | **默认**：读自动放行，写/命令要审批 |
| `auto` | 全放行，但**仍受路径约束** |
| `custom` | interactive + 自动放行 config 里 `auto_allow` 的工具 |

**判决流程**（`permissions.py:evaluate:120`）：

```mermaid
flowchart TD
    CALL["工具调用"] --> RISK["classify: WRITE_LOCAL / EXEC / EXTERNAL / 低危"]
    RISK --> RO{"只读模式且有后果?"}
    RO -->|是| DENY1["拒：只读模式"]
    RO -->|否| WPATH{"是写且指定路径?"}
    WPATH -->|路径不在可写根内| DENY2["拒：不在可写目录"]
    WPATH -->|否/在根内| LOW{"无后果?"}
    LOW -->|是| ALLOW1["放行：低风险"]
    LOW -->|否| AUTO{"auto 模式?"}
    AUTO -->|是| ALLOW2["放行：全权限"]
    AUTO -->|否| ALLOWLIST["查命令白名单 / 会话放行 / §25 标准规则 / custom"]
    ALLOWLIST -->|命中| ALLOW3["放行 + 记审计"]
    ALLOWLIST -->|未命中| ASK["needs_user：弹审批"]
```

**几处安全设计值得单说**：

- **写操作路径约束**：写若指定 `path`，必须落在**可写根**内（`_under_writable_root:204`）。根是**可变、每次检查重读**的，所以运行时增删文件夹立刻生效。
- **shell 白名单防注入**（`permissions.py:_command_allowed:216`）：白名单是"免审批直接跑"，所以前缀匹配**不安全**——`git status` 不能连带放行 `git status && rm -rf ~`。做法：**任何带 shell 操作符（`;` `&` `|` `>` `<` `` ` `` `$(` `(` 换行）的命令直接不走白名单**，然后把 `shlex.split` 出的 argv 和白名单条目做**精确 token 前缀**匹配（`git status` 匹配 `git status -s`，但不匹配 `git statusfoo` 或裸 `git`）。
- **§25 标准规则（standing rules）**（`permissions.py:standing_rule_candidate:62`）：任务作用域的 `{工具: {允许的 target}}`，**只对外部风险（连接器）生效，永不给 exec/写本地**——shell **永远要问**。为什么连接器可以？因为绑定的是**精确 target**（比如"回复到这条 Slack 线程"），精确到 target 才让"自动放行一个连接器工具"变安全。规则从自动化任务里播种、每次检查重读，所以中途点"每次都允许"对本次跑的下一个调用也生效。
- **带外审批（out-of-band）**（`engine.py:_authorize:526`）：引擎只**决策**，把 `needs_user` 交给注入的 `approver`。有人值守时 approver 弹内联卡；无人值守时 approver 是 `inbox_approver`（`inbox.py:348`），把请求变成收件箱项并挂起等人。审批结果分 `once`/`always_tool`/`always_command`/`deny`。
- **`_display` 隐私旁路**（`engine.py:_record_result:642`）：工具结果里的 `_display` 键是**给用户看、但 agent 绝不能看**的元数据（比如"隐私过滤器藏了几条 Gmail 命中"——这个计数模型能拿来试探）。它被抬到消息旁路上、从每个 provider 喂料里剥掉，但持久化给 GUI 的工具卡。用户能看到"隐藏了 N 条 / 剥了 N 个字段值 by 隐私过滤器"的审计，但**只有类别和计数，从无内容**。
- **send_file 独立审批面（§34）**（`agent.py:173`）：把交付物递进聊天有自己的审批面——一条线程对 `send_message` 的标准授权**不覆盖上传文件**。

> 🧠 **一句话**：它对"权限"的执念是**同事级**的：shell 永远要问（连注入都替你堵死），连接器可以按精确 target 免问，agent 连"被隐私过滤器藏了几条"都不许知道。这不是编程助手的"改这个文件行不行"，而是"一个能替你发消息的同事该守什么规矩"。

---

<h2 id="ch8">第 8 章 记忆：显式 SQLite 事实，而非自动压缩</h2>

和 Claude Code 的"自动上下文压缩（microcompact）"不同，**OpenWorker 没有自动压缩上下文窗口**。它的"记忆"是**显式的、持久的 SQLite 事实**：

- 存储：`SQLiteMemoryStore`（`memory/sqlite_store.py:13`），两个作用域 `Scope.GLOBAL` / `Scope.WORKSPACE`，`add/get/list/update/delete`。
- 工具：`remember` / `memory_update` / `memory_forget`（`agent.py:250-260` 装配 `memory_tools`）。
- **何时记的引导**（`agent.py:_MEMORY_GUIDANCE:62`）——没有它，模型要么从不调 `remember`，要么存一堆仓库本就记录的噪音：只存"用户的纠正与偏好（带原因）+ 从代码推不出来的项目背景"；不存仓库已记录的（代码结构、git 历史、AGENTS.md）；用绝对日期，别写"昨天"；存前先查已知记忆，能改就 `memory_update` 别塞近似重复；记忆反映写入时的状态，引用文件/flag/URL 前先核实还在不在。

> 这套"何时记忆"的引导，和本仓库《Claude-Code 深入版》里描述的记忆规范几乎同源——都是"把模型的记忆行为约束成有纪律的持久事实"。

那么长会话的上下文怎么办？靠**两条**：① 回合被 `max_iterations`（默认 150）**有界**；② 历史由 `ConversationStore`（`conversations.py`）持久化、可重载。没有自动摘要丢弃——这是一个明确的取舍：**宁可让人管理记忆（显式 remember），也不让模型自动丢上下文**。对"交付成品的同事"来说，可预测比省 token 重要。

---

<h2 id="ch9">第 9 章 子 agent 与多 agent：persona 即 agent + 只读 explorer + self-wake</h2>

OpenWorker 的"多 agent"有**三个层次**：

**① persona 即 agent。** `agents.get_agent` 委托给 persona 注册表（`agents/registry.py:19`）——**persona 就是 agent**。一个 `Agent`（`agents/base.py:28`）= 名字 + 标题 + 系统提示 + 是否需工作区 + 工具工厂 + `family`（code/knowledge）+ `messaging`/`connectors` 标志。核心 persona：

| persona | 定位 | 家族/工作区 |
|---|---|---|
| **cowork（默认，"OpenWorker"）** | 产出交付物——调研、分析、脚本 | knowledge / deliverable |
| code | 在代码库里干活——文件、git、shell | code / git |
| chat | 快速问答，无工作区（默认从选择器隐藏） | knowledge / none |
| ops（`personas/builtin/ops.md`） | markdown 清单 persona，自吃"清单路径"狗粮 | — |
| myhelper | 遗留的个人助手 persona | — |

**② 只读 explorer 子 agent**（`tools/subagent.py`）——给 code 家族用。宽泛的问题（"重试逻辑在哪处理？"）会把主会话的上下文烧在几十次文件读上。`explore` 工具**开一个子 `TurnEngine`**，同一工作区、只读工具、**全新上下文**，只把**最终报告**返回给调用方，中间的文件读**从不进主上下文**。子引擎强制跑在 **PLAN 模式**（权限引擎硬拦写/shell，不管子 agent 怎么想）、**无 approver**（所以永不需要审批往返）——正因如此 `explore` 能带**低危 metadata**，从而一个回合里的多个 explore **符合并发执行条件**（第 6 章）。**不递归**（子注册表里没有 `explore` 工具）。

```mermaid
flowchart LR
    MAIN["主会话（code persona）<br/>上下文：要改的正事"] -->|explore 宽泛调研| CHILD["子 TurnEngine<br/>全新上下文 · PLAN 只读 · 无审批"]
    CHILD --> SEARCH["grep / read_file / git_log …"]
    SEARCH --> REPORT["只回最终报告（path:line + 关键片段）"]
    REPORT --> MAIN
    MAIN -->|多个 explore 一起请求| PAR["因低危 → 并发跑"]
```

**③ self-wake（自我唤醒）**（`selfwake.py`，`agent.py:240` 给 knowledge 家族装）：knowledge 家族的会话能**挂起自己 + 排一个恢复**（定时 / 完成时 / 事件时）。调度器每个 tick 的 `extra_tick` 恢复到期的 wake（`automation/scheduler.py:85`）。这让一个同事能"我先睡，X 完成了/到点了再叫醒我继续"。

---

<h2 id="ch10">第 10 章 生态：persona 市场、技能、MCP、40 个连接器、十几家模型</h2>

**persona 市场（带同意闸门）**（`personas/registry.py`）：persona 分内置（cowork/code/chat + markdown 的 ops）和第三方。第三方可从**目录**或 **git 仓库**安装（`install_from_dir:340` / `install_from_git:380`），安装时把清单**快照**进托管区（定义稳定、独立于源目录），且**默认禁用 + 未展示、待用户对声明的能力授权后才启用**（`_enabled[id]=False`）——**从不自动启用**。全新安装只有 cowork 启用，其余从设置里 opt-in。清单是带 YAML frontmatter 的 markdown（`PersonaManifest`），`to_agent()` 变成运行时 Agent。

**技能（skills）**：Anthropic 格式的可加载能力，**任何** agent 都能拉进来（区别于 persona——persona 是顶层 surface）。**渐进式披露**：系统提示里先给技能目录清单（`skill_catalog_text`），`load_skill` 工具按需加载全文（`agent.py:262-266`）。技能目录来自 `state_dir/skills` + `工作区/.coworker/skills`。

**MCP**（`mcp/`：client + config + oauth + tools）：自带异步层的 MCP 客户端，支持 stdio + streamable-http，带 **OAuth**（`mcp/oauth.py`）。任何 MCP 可达的工具都能插进来，逐工具控制。

**40 个连接器**（`connectors/descriptors.py` 的 `DESCRIPTORS`）：GitHub、Slack、Jira、Notion、Linear、HubSpot、Outlook、monday.com、Gmail、Google Calendar、Telegram、Asana、Confluence……。加终端和本地文件。连接器令牌存本地密钥库；云端只撮合 OAuth 握手。`integration_tools.py`（4892 行）是最大的单文件——集成工具的重心所在。

**十几家模型**（`providers/`）：`ProviderRouter` 按模型的 `provider:` 前缀路由（`agent.py:214`）。原生 provider：OpenAI、Anthropic（原生 Claude Messages API）、Gemini。加通过 `_openai_compat`（OpenAI 兼容端点）接入的一众：DeepSeek、GLM(Z.ai)、Kimi、Qwen、MiniMax、Grok(xAI)、Mistral，加 **Ollama 全本地**。`providers/matrix.py` 有一张精选的"验证过工具调用"模型清单（`anthropic:claude-opus-4-8`、`gemini:gemini-3.1-pro-preview`、`xai:grok-4.3`……），`capabilities.py` 记每个模型的 vision/pdf 能力（第 5 章的适配就查它）。

```mermaid
flowchart TB
    AGENT["一个 persona 的 TurnEngine"] --> SKILL["技能：Anthropic 格式 · 渐进披露 · load_skill"]
    AGENT --> MCP["MCP 客户端：stdio + http + OAuth"]
    AGENT --> CONN["40 个连接器：GitHub/Slack/Jira/Notion/Gmail/GCal…"]
    AGENT --> PROV["ProviderRouter：按 provider: 前缀路由"]
    PROV --> NATIVE["原生：OpenAI · Anthropic · Gemini"]
    PROV --> COMPAT["OpenAI 兼容端点：DeepSeek/GLM/Kimi/Qwen/MiniMax/Grok/Mistral"]
    PROV --> LOCAL["Ollama：全本地"]
    MARKET["persona 市场"] -->|目录/git 安装 · 快照| GATE["默认禁用 + 待同意"]
    GATE -->|用户授权声明能力| AGENT
```

---

<h2 id="ch11">第 11 章 功能特性：收件箱、定时自动化、换模型、PDF/视觉降级、审计</h2>

**① 收件箱（Inbox）——同事的定义性原语**（`inbox.py`）。跨会话的"人类注意力队列"：当你在一个会话里干活（或人不在、会话无人值守地跑着），收件箱装着**别的 agent 需要你做的事**：审批、提问、通知、目录授权、计划审批。状态机是**反竞态契约**：每项 `pending → resolved`，**只解决一次、幂等、谁先答谁赢**——从任意界面（应用内、Slack、恢复后的输入框）回答都安全（`inbox.py:295`）。**幂等键是 `(session_id, tool_call_id)`**（`inbox.py:133`）：持久恢复重新弹同一个提示时，复用已存在（可能已解决）的项而非重弹——这正是第 6 章持久恢复能成立的原因。

```mermaid
stateDiagram-v2
    [*] --> pending: agent 需要人（审批/提问/授权/计划）
    pending --> resolved: 任意界面首次回答（谁先答谁赢）
    resolved --> [*]: 唤醒挂起的 agent（inbox_approver）
    note right of pending
        幂等键 (session_id, tool_call_id)
        重启后重弹 → 复用同一项，不双跑
        VIS_INLINE 内联 / VIS_INBOX 跨会话
    end note
```

**② 定时自动化（scheduler）**（`automation/scheduler.py:23`）：常驻服务里跑的调度环。策略：**run-once-catch-up**（停机期间漏的到期任务，开机补跑一次再恢复）+ **skip-on-overlap**（上一次还没跑完就不叠一次，`_running_ids` 守）。每个到期任务**只 spawn 不 await**（`_tick:76`）——因为一个跑法可能挂在收件箱的待审批上，**一个卡住的自动化绝不能堵住调度环、别的到期任务、或 self-wake 恢复**。next_run 用 croniter 算。

```mermaid
flowchart LR
    TICK["每 30s tick（首次=补跑）"] --> DUE["store.due() 到期任务"]
    DUE --> SPAWN["只 spawn 不 await（重叠跳过）"]
    SPAWN --> RUN["跑一个自动化会话"]
    RUN --> SUSPEND{"要审批?"}
    SUSPEND -->|是| PARK["挂进收件箱，不堵调度环"]
    SUSPEND -->|否| DELIVER["产出成品 + 逐字记录落应用"]
    TICK --> WAKE["extra_tick：恢复到期 self-wake"]
```

**③ 会话中途换模型**（`engine.py:switch_model:180`）：历史是规范 OpenAI 形状、每 provider 每次转换，所以换模型只是**改个字段** + 一条"在哪换的" notice。若历史带图而新模型无视觉，notice 追加降级警告（那些图按占位符发，见第 5 章）。

**④ PDF/视觉能力降级**（第 5 章）：每次调用按当前模型重判，原生给真货、否则本地降级——是"随便换模型"的配套。

**⑤ 审计轨迹**（`engine.py:_audit:690`）：每个工具调用在多个阶段被审计（proposed / auto_allowed / started / finished / approval_requested / approval_resolved / filtered……）。**§25 不变式：每个被自动放行的调用都要引用它的规则。**

**⑥ 本地优先与密钥**（`secrets.py`）：模型密钥、连接器令牌全在本地密钥库；不登录也能用（手填凭证）。

**⑦ 语音输入**：`stt/`（Rust）语音转文字侧车。**⑧ 自更新**：README 说 App 会自我更新，修复很快到装机。

---

<h2 id="ch12">第 12 章 总结：三个最独特的设计与取舍</h2>

### 三件事（如果只记三个设计）

**1. 收件箱 + 持久恢复 = "无人值守 AI 同事"的地基。**
跨会话的人类注意力队列，`pending→resolved` 只解决一次、幂等（按 `session_id+tool_call_id`）、谁先答谁赢、任意界面可答；有人值守内联、无人值守入箱。回合能在提示处**挂起并持久化**，服务重启后**从那条"待答"续上、不双跑**。别人的 agent 循环停在"我做完了"，OpenWorker 的能停在"我需要你点个头"——而且这个"停"扛得住重启。

**2. Provider 无关（自研 provider 层，aisuite 仅用于工具层）。**
历史是 OpenAI 形状，每 provider 每次转换；会话中途换模型只是改字段；PDF/视觉能力**每回合按当前模型重判**（原生给真货，否则本地抽文/转图/占位符）。"随时换模型""附件随模型升降级"是免费副产品。`_display` 隐私旁路更狠——**agent 连"被隐私过滤器藏了几条"都不许知道**。

**3. 同事级审批治理闭环。**
调度器（补跑一次 + 重叠跳过 + 只 spawn 不 await）+ self-wake（定时/完成/事件自我唤醒）+ §25 标准规则（**仅外部风险、精确 target；shell 永远要问**，连 `&&` 注入都替你堵死）+ @提及即线程授权（`thread_target` 一串三用、重启存活）。四者合起来：**定时无人值守干活、把 ask 安全停在收件箱、绝不越权开 shell**。

### 取舍与"适合谁"

| 维度 | OpenWorker 的选择 | 代价 / 适合谁 |
|---|---|---|
| 目标 | 交付成品的**日常同事**（跨应用） | 不是为写代码优化；纯编程场景不如 opencode/Claude Code 顺手 |
| 部署 | **本地优先**桌面 App（Tauri+Python+Rust） | 装机重（要 Python+Node+Rust 从源码跑）；但数据只经你选的模型/集成出门 |
| 模型 | **provider 无关**（含 Ollama 本地） | 自研 provider 层（原生 + OpenAI 兼容端点）；aisuite 仅管工具层 |
| 权限 | **同事级治理**（收件箱/审批/标准规则/隐私旁路） | 复杂度高；换来"敢让它无人值守替你发消息" |
| 上下文 | 显式 SQLite 记忆，**不自动压缩** | 长会话靠有界迭代 + 持久化；可预测优先于省 token |
| 记忆 | 全局/工作区双 scope + 何时记引导 | 需要模型有纪律地 `remember` |

**一句话选型**：想要一个**能接 Slack、按点自动跑、动手前会问、跑完给成品、还能全本地**的"数字同事"——OpenWorker 是目前开源里把这件事想得最完整的。想要一个**纯粹会写代码**的终端 agent，回去看 opencode / Claude Code / grok-build。它和 **Manus 系（OpenManus/Suna）**最像（都是通用任务），但 OpenWorker 的重心是**桌面本地 + 审批治理 + 连接器 + 定时**，Manus 系的重心是**云端沙箱 + 平台化**——一个是"住你电脑里的同事"，一个是"云上的任务工人"。

### 🔍 源码指路（回源码核对）

| 想看 | 去这里 |
|---|---|
| 主循环（并发/中断/持久恢复） | `coworker/engine.py:52,156,294,437,120,250` |
| 权限判决 + shell 防注入 + §25 | `coworker/permissions.py:120,216,62` |
| 引擎装配（按家族分装工具） | `coworker/agent.py:109,217,228` |
| 收件箱状态机 + 带外审批 | `coworker/inbox.py:61,295,133,348` |
| 定时调度器 | `coworker/automation/scheduler.py:23,63,76` |
| 只读 explorer 子 agent | `coworker/tools/subagent.py:42,86` |
| persona 市场（同意闸门） | `coworker/personas/registry.py:340,:363`（`:123` 为 builder 注册） |
| @提及→线程授权（一串三用） | `coworker/mentions.py` |
| provider 路由 + 能力矩阵 | `coworker/providers/router.py · matrix.py · capabilities.py` |
| 启动侧车 + 孤儿守卫 | `coworker/server/run.py` · `cli.py` |

---

*本分析基于 `andrewyng/openworker` commit `db93d75`（2026-07-25）实际检出源码撰写，所有结论标注 `文件:行号` 可回源核对。与本系列其他分析同框架。*
