# OpenClaw v2026.5.14 源码全貌参考

> **本文档定位**：与 [`claude-code-reference.md`](./claude-code-reference.md) 对标，为 FFAI Agent 内核（参见 [`00-architecture.md`](./00-architecture.md) v0.1）提供**第二份业界参考实现**。
>
> **研究对象**：[OpenClaw](https://github.com/openclaw/openclaw) v2026.5.14（2026-05-15 从上游 `main` 浅克隆）。
> 定位：**Multi-channel AI gateway**——把 LLM agent 接到 WhatsApp/Telegram/Slack/Discord/iMessage/Signal/IRC/LINE/Microsoft Teams/Matrix/Feishu/Google Chat/Mattermost/Nextcloud Talk/Nostr 等 20+ 通讯渠道，本地运行、用户自托管。许可证 MIT，TypeScript 单仓多包（pnpm workspace）。
>
> **报告生成方式**：4 个并行 general-purpose agent，按内核 / 渠道 / 扩展性 / 能力&UI&基础设施 四象限扫 `/tmp/openclaw-fresh/`（不含 lock 文件、build 产物、git 历史）。
>
> **使用方式**：与 `claude-code-reference.md` 互补——Claude Code 是 **TUI / 单用户 / Codex provider / 编程场景**；OpenClaw 是 **多渠道 IM / 单用户但多设备 / 多 provider / 个人助手场景**。我们自研的 FFAI Agent 是 **Web / 多租户 / 多 provider / 企业工作流场景**，差异点见 `00-architecture.md` §4–§5。本文档**不要照抄**，按子系统决策时回查。
>
> **关键反直觉发现（先放最前面）**：
> 1. 上游 v2026.5.14 与本地 `~/Code/openclaw` 旧版（v2026.3.13）结构差异巨大——src/ 从 53 → 104 子目录，**所有渠道实现已从 src/ 移到 extensions/**，src/channels/ 只剩抽象。研究 openclaw 必须 clone 最新。
> 2. `src/web/` 和 `src/channels/web/` 不是 "Web Chat"，是 **WhatsApp Web QR 配对模式**。
> 3. Web UI 用 **Lit 3.3.2 + Vite 8**，不是 React。单 WebSocket 通道，不用 SSE。
> 4. TUI 用 `@earendil-works/pi-tui`，**不是 ink**。
> 5. 移动端 iOS = SwiftUI、Android = Kotlin + Compose，**完全原生**，不是 RN/Capacitor。
> 6. `src/tools/`（v2026.5.14 新增）是**纯元模型层**——不实现任何工具，只定义 ToolDescriptor + availability 表达式 + planner。具体工具实现仍走 `extensions/<plugin>/`。
> 7. Canvas 协议（A2UI）用 **JSONL 增量** + **5 个 surface action**；agent 通过单一 `canvas` 工具的 7 个 action（含 `snapshot`）主动渲染+回看，形成**视觉闭环**。
> 8. 概念命名密集容易混：**chat / flows / trajectory / tasks / status / talk** 是 6 个不同子系统（见 §1.x）；**plugin / extension / skill** 是 3 种不同的扩展机制（见 §3.x）。
>
> **目录**：
> - [一、内核与运行时](#一内核与运行时openclaw-v202651-上游研究)
> - [二、渠道与消息](#二渠道与消息)
> - [三、扩展性](#三扩展性层openclaw-v202651)
> - [四 / 五 / 六、能力工具 / UI 多端 / 基础设施](#openclaw-v202651--能力工具--ui-多端--基础设施-与-运维)

---

# 〇、整体架构总览

> 这一节是**全局视图**，把后面四象限串成一张图。先读这节再读 §一-§六。
> 关键事实来自后续分章节，源码定位见各小节内 `path:line` 引用。

## 0.1 OpenClaw 一句话定位

> **一个本地运行的"多渠道 AI 网关 + 个人 agent 助手"**——把 LLM agent（多数情况下宿主 Claude Code / Codex / Gemini CLI）挂到用户已经在用的 IM 渠道上（WhatsApp/Telegram/Slack/Discord/iMessage 等 20+），同时提供 TUI / Web UI / macOS / iOS / Android 多端入口，所有能力以 **plugin/extension/skill** 形式装载、运行时即热即用。

定位上的几个反直觉点（决定了所有架构选择）：

1. **单用户、多设备、多渠道**——不是 SaaS，每个 OpenClaw 实例服务**一个人**。所以没有"多租户"概念，但有"同一个人从 5 个渠道发消息要不要算一个会话"的合并问题（→ §1.4 session、§2.6 pairing）。
2. **gateway 不直接跑 LLM**——大部分情况下它是个 **"CLI 宿主"**：把本地装好的 `claude` / `codex` / `gemini` 二进制拉起来当后端用，自己负责 IO 转发、上下文工程、故障转移（→ §1.2 agents/cli-runner）。这跟 Claude Code 自己实现 master-loop 是反过来的——OpenClaw 是 Claude Code 的**外壳**之一。
3. **core 精简、能力外置**——`src/` 只有内核、协议、抽象；`extensions/` 有 **133 个 bundled plugin**（含全部 LLM provider、全部渠道、全部工具）；`skills/` **53 个**。core 不知道 "Discord" 长什么样，只知道 "Channel" 抽象（→ §3.5 extensions、§二 渠道层）。
4. **agent 可以主动渲染 UI**——通过 **A2UI 协议（JSONL 增量）**+ Canvas extension，agent 用一个 `canvas` 工具就能让用户屏幕上"凭空出现"一个 surface（表单、表格、地图、富文本）（→ §4.6）。这是与 Claude Code TUI 最大的形态差异。

## 0.2 七层架构图

```
┌────────────────────────────────────────────────────────────────────────────────┐
│  L7  入口层  CLI / TUI / Web UI(Lit+Vite) / macOS / iOS(SwiftUI) / Android(Compose) │
│              + 通过 IM 渠道间接入口（WhatsApp/Telegram/Slack/...）                │
└──────────────┬─────────────────────────────────────────────────────────────────┘
               │  WebSocket(单通道，不用 SSE) + HTTP fallback
┌──────────────▼─────────────────────────────────────────────────────────────────┐
│  L6  控制面    Gateway(HTTP/WS server) / Daemon(launchd/systemd 守护) / node-host   │
│              + ACP server(对外暴露成 Agent Client) + MCP server(暴露 channel 为 tool)│
└──────────────┬─────────────────────────────────────────────────────────────────┘
               │
┌──────────────▼─────────────────────────────────────────────────────────────────┐
│  L5  渠道层   channels 抽象(Inbound/Outbound/Turn 状态机/Normalize)              │
│              + auto-reply(入站调度) + pairing(DM 准入/QR 配对) + polls(轮询)        │
└──────────────┬─────────────────────────────────────────────────────────────────┘
               │  统一化后的 Message
┌──────────────▼─────────────────────────────────────────────────────────────────┐
│  L4  Agent loop / 会话 / 上下文                                                  │
│   ┌────────────────┬──────────────┬──────────────┬─────────────┬────────────┐   │
│   │ agents/        │ sessions/     │ context-     │ trajectory/ │ tasks/     │   │
│   │ (主循环 +      │ (sessionId    │ engine/      │ (事件流持   │ (detached  │   │
│   │  cli-runner +  │  + send-      │ (transcript  │  久化用于   │  long-     │   │
│   │  toolset)      │  policy +     │  compaction) │  trace)     │  running)  │   │
│   │                │  pairing 桥)   │              │             │            │   │
│   └────────────────┴──────────────┴──────────────┴─────────────┴────────────┘   │
│   ┌────────────────┬──────────────┬──────────────┬─────────────┬────────────┐   │
│   │ chat(View)     │ flows(Wizard)│ status(Notify)│ talk(Voice) │ interactive│   │
│   └────────────────┴──────────────┴──────────────┴─────────────┴────────────┘   │
└──────────────┬─────────────────────────────────────────────────────────────────┘
               │
┌──────────────▼─────────────────────────────────────────────────────────────────┐
│  L3  Provider / Routing                                                          │
│              model-catalog(模型注册表) + provider-runtime(失败转移) + routing     │
│              + auth-profiles(OAuth/API key 多档轮换)                              │
└──────────────┬─────────────────────────────────────────────────────────────────┘
               │
┌──────────────▼─────────────────────────────────────────────────────────────────┐
│  L2  能力工具层                                                                  │
│   web-search │ web-fetch │ image-gen │ music-gen │ video-gen │ realtime-trans-  │
│              │           │           │           │           │ cription          │
│   tts │ markdown │ terminal │ media-understand │ link-understand │ canvas(A2UI)   │
│              tools/(描述符+planner，不实现工具) → 实际实现在 extensions/         │
└──────────────┬─────────────────────────────────────────────────────────────────┘
               │
┌──────────────▼─────────────────────────────────────────────────────────────────┐
│  L1  扩展性                                                                       │
│        plugins(runtime) + plugin-sdk(契约) + plugin-package-contract(npm 包)     │
│        + hooks(Pre/Post 钩子) + commands(slash) + cli + wizard(首装向导)         │
│        + extensions/(133 bundled) + skills/(53 bundled) + ClawHub(远程 registry) │
└──────────────┬─────────────────────────────────────────────────────────────────┘
               │
┌──────────────▼─────────────────────────────────────────────────────────────────┐
│  L0  基础设施                                                                    │
│        cron(调度) │ secrets(凭据矩阵) │ security(审计) │ sessions(持久化)        │
│        infra(563 文件通用基设) │ logging │ proxy-capture │ i18n │ config         │
│        memory(host-sdk + memory-core/lancedb/wiki/active-memory)                 │
└────────────────────────────────────────────────────────────────────────────────┘
```

## 0.3 一条入站消息的完整旅程（数据流）

```
  ① 用户在 WhatsApp 给 OpenClaw bot 发消息
        │
        ▼
  ② extensions/whatsapp/ 接收 webhook，生成 RawInboundMessage
        │
        ▼
  ③ channels/plugins/normalize/ 归一化成统一 NormalizedMessage
     - 抽 sender、attachment、quote、reply-to、locale
     - 解 pairing token（这条 WhatsApp 来自哪个 OpenClaw 用户身份）
        │
        ▼
  ④ pairing/ 准入校验
     - 是否已配对？是否在 allowlist？
     - 未配对 → 走 onboarding 流程（QR/code/邀请码）
        │
        ▼
  ⑤ channels/turn/ 状态机决定"这是新对话还是续聊"
     - 计算 Turn ID = (channel, threadId, sender) 哈希
     - 拼接历史 facts 给 agent
        │
        ▼
  ⑥ auto-reply/ inbound dispatcher
     - 命令？(/help) → command 系统直接回
     - 心跳？ → heartbeat job
     - 普通消息 → 投递给 agent
        │
        ▼
  ⑦ sessions/ 拿到 sessionId，查 send-policy（要不要回？要回几条？）
        │
        ▼
  ⑧ agents/ 主循环（多数情况下：cli-runner 拉起 claude/codex 子进程）
     - 拼 system prompt（context-engine 处理压缩 + 注入 trajectory）
     - 加载 tools（来自 tools/ 描述符 + extensions/ 实现）
     - 调用 model（model-catalog → provider-runtime → 失败转移）
        │
        ▼
  ⑨ agent 输出
     - 文本片段 → trajectory/ 记录 → 走 channels/plugins/outbound/ 投递回 WhatsApp
     - 工具调用 → 走 hooks(PreToolUse) → 执行 → hooks(PostToolUse)
        │   特殊：canvas 工具 → 通过 A2UI 协议 push 到用户的 Web UI / 移动 App surface
        │
        ▼
  ⑩ 出站投递
     - channels/plugins/outbound/ 把回复拆分（长文本/富媒体）
     - 经过同一个渠道（WhatsApp）或路由到其他渠道（"把结果发我邮箱"）
     - status/ 广播给所有 surface（TUI 顶部 status line / Web UI badge / 移动通知）
```

## 0.4 三种扩展机制（plugin / extension / skill）的关系

容易混的地方：openclaw 有**三种**扩展方式，定位完全不同。

| 机制 | 形态 | 加载方式 | 典型例子 | 类比 |
|---|---|---|---|---|
| **plugin** | TypeScript 代码 + manifest | 通过 `plugin-sdk` 类型注册 hook/command/tool；npm 包或 bundled | LLM provider（anthropic/openai/codex）、渠道（discord/feishu）、工具（exa/firecrawl） | VSCode 插件 |
| **extension** | plugin 的**分发形态**之一 | bundled 在 `extensions/` 或外部 npm 包 | 全部 133 个 bundled extensions（每个本质上还是一个 plugin） | "把 plugin 装到磁盘上" |
| **skill** | Markdown frontmatter + 可选脚本 | 通过 `skills/` 目录文件加载或 ClawHub 拉取；只描述能力意图，不写代码 | coding-agent / canvas / weather / github | Claude Code Skills |

**用一句话区分**：**plugin/extension 给 agent 提供"能"做什么的代码**；**skill 给 agent 提供"该不该 / 怎么做"的提示和工作流**。skill 触发后通常调用一个或多个 plugin 暴露的 tool。

## 0.5 与 Claude Code 的形态对比（决策时回看）

| 维度 | Claude Code 2.1 | OpenClaw v2026.5 | 我们的 FFAI Agent |
|---|---|---|---|
| 形态 | TUI（终端原生） | TUI + Web UI(Lit) + iOS/Android 原生 | Web 应用（Next.js） |
| 部署 | 单二进制 npm 包 | 单仓多包 + daemon | NestJS 多租户 SaaS |
| Agent 循环 | 自实现 master-loop（single-threaded） | **宿主** claude/codex/gemini CLI（多数情况）| 自实现（参考 Claude Code 与 OpenClaw 都借鉴） |
| Provider | 仅 Anthropic | 多 provider（41 个 LLM extension）+ failover 链 | 多 provider（OpenAI/Anthropic/...）+ failover |
| 用户模型 | 单用户 | 单用户多设备 | **多租户多用户多角色** |
| 入站源 | TTY stdin | TTY + WebSocket + 20+ IM 渠道 | Web UI（暂时） |
| 工具发现 | 内置 43 个 tool | 工具描述符在 `src/tools/`，实现散落在 extensions | 待设计——参考 OpenClaw 的 ToolDescriptor + availability 表达式 |
| Skill | 内置 + 用户 `.claude/skills/` | 内置 + 用户 + ClawHub 远程 registry | 内置 + 组织自定义（数据库） |
| 工件渲染 | TUI 静态 markdown | **A2UI Canvas（动态 surface push）** | 想抄 Canvas |
| 鉴权 | API key | OAuth 多 profile + API key 矩阵 | 现有企业 SSO |
| 持久化 | JSONL + 文件 | SQLite + 文件 + memory-host-sdk | PostgreSQL + Prisma |

**对我们最大启示**：
- 不必自己写 agent 主循环——可以宿主 Claude Code（我们 00-architecture.md Q4 已经定了"只接 Codex"，但 OpenClaw 启示我们更灵活：把 CLI runner 做成可插拔，今天 Codex，明天换 Claude）。
- **抽象层（context-engine 接口、failover 策略、Trajectory 事件 schema、Turn 状态机、A2UI 协议、ToolDescriptor + availability 表达式）几乎都可以直接抄字段定义**。
- 多渠道我们暂不做，但 **Channel 接口 + Normalize + Outbound 三件套**值得抄成"未来接 IM 时插槽已经留好"的样子。
- skill 三源加载（内置 + 用户 + ClawHub）启发我们的"研发预置 + 组织自定义双源"可以预留**第三源（marketplace）**作为未来扩展点。

## 0.6 阅读路线建议

- **只想 5 分钟了解 OpenClaw**：读完本节 §0 即可。
- **设计我们的 agent 主循环 / context engine**：§一（特别是 §1.2 / §1.3 / §1.4）。
- **未来接 IM 渠道时**：§二（特别是 §2.1 normalize / §2.4 outbound）。
- **设计 plugin/skill 体系**：§三（特别是 §3.0 三层金字塔 / §3.5 extensions / §3.10 skills）。
- **设计 Canvas/工件展示**：§4.6（A2UI 协议）+ §5.2（Web UI 怎么承载）。
- **设计基础设施层（cron/secrets/memory）**：§六。

---

# 一、内核与运行时（OpenClaw v2026.5.14 上游研究）

> 综述：本层是 OpenClaw "Multi-channel AI gateway" 的**核心抽象**。v2026.5.14
> 已经把"channel 适配（Discord/WhatsApp/SMS/邮件/Slack 等 20+ 渠道）"全部下沉到
> `extensions/`，`src/` 只保留**内核 + 协议 + 运行时**，更适合做参考。整体定位类似
> 一个**"宿主 Claude/Codex/Gemini CLI 的多 agent 网关"**——它不是自己实现 LLM
> agent 主循环，而是通过 `cli-runner` 把真正的 CLI（claude/codex/gemini）当
> 后端拉起，自己负责**会话路由、上下文工程、provider 故障转移、跨渠道桥接、ACP/MCP
> 协议出入口、commitment（承诺）跟踪、daemon/gateway 化**。对应到 Claude Code 自身
> 仓库的概念：`src/gateway/` ≈ `lib/` 中的 SSE/HTTP server，`src/agents/` ≈ Claude
> Code 的 master-loop+tools，`src/context-engine/` ≈ Claude Code 的 transcript
> compaction，`src/acp/` 是反向把 OpenClaw 暴露成 ACP server，`src/mcp/` 反过来
> 把 channel 暴露成 MCP tool。**对我们 NestJS 自研 web agent 的最大启发**是：
> agent loop 不必自己写——可以宿主成熟 CLI；但**抽象层（context-engine 接口、
> failover 错误、commitment 状态机、ACP/MCP 双向桥）非常值得借鉴**。

---

## 1.1 进程入口与运行时基座

### 职责
- 区分"CLI 主进程 / 被 import 的 library / daemon 子进程"三种身份，按需重生（respawn）。
- 注入运行时输出抽象（`RuntimeEnv`），让所有 CLI 命令通过同一个 `log/error/exit/writeJson` 接口出去——便于测试 mock 和 daemon 转发。
- 通过 `entry.compile-cache` + `entry.version-fast-path` + `entry.respawn` 把启动路径极致压扁（gateway 启动有专门 startup trace）。

### 关键文件
- `src/entry.ts:1-220`：CLI 主入口，按 wrapper 名（`openclaw.mjs`/`openclaw.js`）判定是否为 main module，再决定是否走 respawn / version fast-path / root-help fast-path。
- `src/index.ts:1-132`：**legacy 入口 + library 导出双重身份**——`isMainModule` 为 true 时挂全局 `uncaughtException` 处理器并跑 `runLegacyCliEntry`；为 false 时把 `library.ts` 的所有导出 lazy-binding 上来。
- `src/library.ts:1-91`：**library 出口**。所有 export 都是 lazy import（`replyRuntimePromise` 这种模式）——SDK 用户 `import { loadConfig } from "openclaw"` 不会把整棵依赖图拉起。
- `src/runtime.ts:1-117`：`RuntimeEnv` / `OutputRuntimeEnv` 接口 + `defaultRuntime` 单例；处理 `EPIPE`/`EIO`、vitest mock 检测、`clearActiveProgressLine` 与 stdout 协作。
- `src/globals.ts:1-37`、`src/global-state.ts:1-18`：仅放 verbose/yes 两个最小全局，避免全局污染。
- `src/entry.respawn.ts`、`src/entry.compile-cache.ts`、`src/entry.version-fast-path.ts`：respawn 计划、Node compile cache 启用/取消、`--version` 快路径绕过完整 CLI 装载。
- `src/extensionAPI.ts:1-37`：暴露给 `extensions/` 的稳定面（channel 插件能看到的内核 API）。

### 数据结构 / 接口

```ts
// runtime.ts
export type RuntimeEnv = {
  log:   (...args: unknown[]) => void;
  error: (...args: unknown[]) => void;
  exit:  (code: number) => void;
};
export type OutputRuntimeEnv = RuntimeEnv & {
  writeStdout: (value: string) => void;
  writeJson:   (value: unknown, space?: number) => void;
};
```

### 调用链

```
node openclaw.mjs <argv>
   └─ entry.ts (isMainModule check)
       ├─ respawnWithoutOpenClawCompileCacheIfNeeded   // 解决 compile-cache 不兼容旧 node
       ├─ enableOpenClawCompileCache + warningFilter + normalizeEnv
       ├─ parseCliContainerArgs / parseCliProfileArgs  // --container / --profile/--dev
       ├─ tryHandleRootVersionFastPath                 // -v / --version 直接退出
       └─ runMainOrRootHelp
            ├─ tryHandleRootHelpFastPath (precomputed help text)
            └─ dynamic import("./cli/run-main.js") → runCli(argv)
```

### 可借鉴点

- **可借**：`RuntimeEnv` 这种把 `log/error/exit/writeJson` 抽成接口的做法——NestJS
  里通常依赖 `Logger` 服务，但**对 CLI/脚本类入口**（我们的 ops/seed/migrate 脚本）
  非常合适，便于单测断言。
- **可借**：`library.ts` lazy import 模式——我们如果做 `@openclaw/sdk` 一类二次封装
  给前端/insomnia/playground 用，也该这么写，避免一次性把 NestJS DI 拉起。
- **不抄**：respawn / compile-cache fast-path 这一套是 Node CLI 启动开销的极致优化，
  跟我们 long-running NestJS server 完全无关。

---

## 1.2 Agent loop：`src/agents/`（"宿主 CLI" 主循环）

### 职责
- **不自己跑 agent 主循环**——通过 `cli-runner` 把 Claude/Codex/Gemini CLI 当子进程拉起，把 OpenClaw 自己的"渠道工具"通过 MCP/bundle 注入。
- 负责**provider/auth profile 解析、API key rotation、cooldown 探测、failover、bootstrap prompt 注入、context window 守护、compaction 触发、hook 调度**。

### 关键文件
- `src/agents/cli-runner.ts:75-129`：`runCliAgent`——agent 单次回复的统一入口。先跑 `before_agent_reply` hook（cron 触发场景），再 `prepareCliRunContext`，再 `runPreparedCliAgent`。
- `src/agents/cli-runner/execute.ts:263+`：`executePreparedCliRun`——真正构造 CLI 子进程 argv、写 system prompt 临时文件、注入 images、`enqueueCliRun` 串行化、设置 abort signal。
- `src/agents/agent-command.ts:1392`：`agentCommand` / `agentCommandFromIngress`——上游入口（HTTP/cron/channel ingress 都走这里）。
- `src/agents/agent-runtime-config.ts:8-48`：`resolveAgentRuntimeConfig`——把 config + secret refs 解析合并，调用 `setRuntimeConfigSnapshot`，是 agent 跑前的必经步骤。
- `src/agents/failover-error.ts:16-60`：`FailoverError` class，携带 `reason/provider/model/profileId/status/code/sessionId/lane/suspend`——多 provider/多 key 故障转移的统一错误载体。
- `src/agents/failover-policy.ts`、`src/agents/configured-provider-fallback.ts:8-39`：fallback 链解析。
- `src/agents/auth-profiles/oauth-manager.ts`、`oauth.ts`、`oauth-refresh-queue.ts`：**多账号 OAuth 池**——多个 Claude/ChatGPT 账号轮转刷新，遇到 quota cooldown 自动切下一个。
- `src/agents/compaction.ts`：触发上下文压缩。
- `src/agents/context.ts`、`context-window-guard.ts`、`context-cache.ts`：上下文窗口估算 + 守护。
- `src/agents/bootstrap-*.ts`：开机 prompt 注入（很多 channel 场景需要"这是第一条消息"的引导）。
- `src/agents/bash-tools.exec*.ts`：~30 个文件全是 bash tool 的执行/审批/host gateway 实现，跟 Claude Code 的 Bash tool 几乎对应。

### 数据结构 / 接口（提炼）

```ts
// FailoverError 携带的字段 ≈ 业界 "agent loop 跨 provider 错误协议"
class FailoverError extends Error {
  reason: FailoverReason;     // 决策依据
  provider/model/profileId;   // 上下文
  status/code;                // HTTP/SDK 原始
  sessionId/lane;             // 回溯到 session 的归属
  suspend?: boolean;          // 是否把这个 profile 暂时熔断
}

// 单次 agent 调用参数（从 cli-runner.ts 提炼）
type RunCliAgentParams = {
  runId; jobId; agentId;
  sessionKey; sessionId; sessionFile;
  workspaceDir;
  trigger: "cron" | "user" | "channel" | ...;
  prompt; images?;
  provider; model?;
  skillsSnapshot?;
  abortSignal?;
  ...channel-fields
};
```

### 调用链

```
ingress (HTTP/channel/cron)
   └─ agentCommandFromIngress
       └─ agentCommand
           ├─ resolveAgentRuntimeConfig (secrets + snapshot)
           ├─ resolveCliBackend (claude / codex / gemini / custom)
           ├─ hook: before_agent_reply (可拦截直接 reply)
           └─ runCliAgent
               ├─ prepareCliRunContext (auth profile + workspace + bootstrap)
               └─ runPreparedCliAgent
                   ├─ hook: llm_input / before_agent_run
                   ├─ executePreparedCliRun
                   │   ├─ writeCliSystemPromptFile (临时文件)
                   │   ├─ prepareCliPromptImagePayload
                   │   ├─ buildCliArgs(model/sessionId/...)
                   │   ├─ enqueueCliRun (per-session 串行队列)
                   │   └─ spawn child process (claude/codex/gemini)
                   │       └─ stream stdout → events
                   ├─ hook: llm_output / agent_end
                   └─ FailoverError 捕获 → 切下一个 provider/key
```

### 可借鉴点

- **强烈可借**：`FailoverError` 这个**结构化错误协议**——我们 NestJS 多 provider
  适配里完全可以照搬：`{ reason, provider, model, profileId, status, code,
  sessionId, suspend }`。relay/熔断/告警/日志归因都从这一个对象拿。
- **可借**：`auth-profiles` 多账号 OAuth 池（refresh queue + lock + cooldown），对应
  我们多租户里"一个 tenant 配多个 API key 自动轮转"——值得抄整套状态机
  （key 状态 `active / cooling / suspended / refreshing`，cooldown 过期自动唤醒）。
- **可借**：`enqueueCliRun(queueKey, ...)` 的 **per-session 串行化**——一个 sessionKey
  同一时间只能一个 turn 在跑。我们也该在 BullMQ/in-memory 加这层 lock。
- **不抄**：`cli-runner` 整套 "spawn 真 CLI 当后端" 的范式不适合我们——我们的
  agent 直接调 SDK，没有"宿主第三方 CLI"的需求。但**hook 体系**
  （`before_agent_reply / llm_input / llm_output / agent_end /
  before_agent_run`）这一套观测点是值得抄的——加到我们的 NestJS interceptor 即可。

### 跟 Claude Code master-loop 的差异

| 维度 | Claude Code master-loop | OpenClaw cli-runner |
| --- | --- | --- |
| **谁跑 loop** | 自己跑 tool-use → assistant turn 循环 | 把 Claude CLI 当子进程，loop 在子进程里 |
| **tool 注入** | 直接 register 到 master-loop | 通过 MCP/bundle-mcp 注入到 CLI |
| **session 状态** | 内存 + transcript 文件 | sessionFile 是 CLI 的，OpenClaw 同时维护 ledger/registry |
| **多 provider** | 切 model 即可 | 切 backend（claude/codex/gemini），每个有自己的 argv 协议 |
| **hooks** | 内建 hook system | 同样有 5 类 hook，但通过子进程 IPC + 内存两路 |

---

## 1.3 六个子系统的边界（chat / flows / trajectory / tasks / status / talk）

这几个目录命名很容易混，**实际职责完全不重叠**：

| 子系统 | 职责（1 句话） | 关键文件 | 等价物 |
| --- | --- | --- | --- |
| **chat** | 仅放"聊天 UI 渲染辅助"——canvas/工具调用内容格式化，**不是 chat agent** | `src/chat/canvas-render.ts`、`src/chat/tool-content.ts`（**整个目录就 2 个文件**） | 视图层 helper |
| **flows** | 高层"setup 流程"——channel 接入向导、provider 选择向导、doctor 健康检查、search 配置 | `flows/channel-setup.ts`、`flows/provider-flow.ts`、`flows/doctor-health.ts`、`flows/model-picker.ts` | 类似 NestJS 的 "wizard service"，跨多步交互 |
| **trajectory** | **追溯/导出 agent 跑轨迹**（事件流持久化）——给 debugging/replay/audit 用 | `trajectory/types.ts:9-28`（`TrajectoryEvent`、`schemaVersion:1`、`source: runtime/transcript/export`） | OpenTelemetry trace 的应用层版 |
| **tasks** | **detached / long-running 任务调度** + task registry/audit（SQLite store） | `tasks/task-executor.ts`、`tasks/task-registry.store.sqlite.ts`、`tasks/task-flow-registry.ts`、`tasks/detached-task-runtime.ts` | 类似 Temporal 但本地 SQLite |
| **status** | **status bar / 后台 agent 状态消息**渲染——给 TUI 顶部 status line / channel 转发用 | `status/status-message.ts`、`status/status-queue.runtime.ts`、`status/status-subagents.runtime.ts`、`status/agent-runtime-label.ts` | "状态广播" 总线 |
| **talk** | **realtime voice / 双向语音对话**——音频 codec + provider registry（独立于文本 agent） | `talk/audio-codec.ts`、`talk/talk-session-controller.ts`、`talk/provider-registry.ts`、`talk/session-runtime.ts`、`talk/agent-consult-tool.ts` | 类似 OpenAI Realtime API 封装 |

**记忆口诀**：chat=View、flows=Wizard、trajectory=Trace、tasks=Job、status=Notify、talk=Voice。

### 可借鉴点
- **强烈可借**：`TrajectoryEvent` 的 schema（`traceId/sessionId/runId/source/seq/sourceSeq`）
  正是我们多租户 agent 调试追溯需要的最小集——可以直接抄字段定义。
- **可借**：六个目录的**职责切分**——我们 NestJS 模块设计时也应该按这种"动词"
  切，而不是按"功能"乱堆（"AgentModule" 里塞所有东西的反面教材）。
- **不抄**：talk 模块对我们 web agent 不直接需要，但其 `provider-registry.ts`
  做"多 provider voice 后端注册表"的模式可以参考。

---

## 1.4 Sessions / Context Engine（上下文工程）

### 职责
- `sessions/`：**会话级别**的元数据——sessionId 解析、send policy（限流/去重）、input 来源追踪、transcript 事件流、生命周期事件。
- `context-engine/`：**可插拔的"上下文管理引擎"接口**——bootstrap / ingest / assemble / compact / afterTurn / maintain 全生命周期。

### 关键文件
- `src/context-engine/types.ts:190-326`：`ContextEngine` interface——必读。
- `src/context-engine/registry.ts`、`delegate.ts`、`init.ts`、`legacy.ts`、`legacy.registration.ts`：注册 / delegate / 旧引擎兼容。
- `src/sessions/session-id.ts`、`session-key-utils.ts`、`classify-session-kind.ts`：sessionId/sessionKey 解析（two-level：key=语义/逻辑，id=物理 transcript 文件）。
- `src/sessions/send-policy.ts`：限流/去重策略。
- `src/sessions/transcript-events.ts`、`session-lifecycle-events.ts`：transcript 事件 schema。

### 数据结构 / 接口

```ts
// context-engine/types.ts 提炼
interface ContextEngine {
  info: ContextEngineInfo;       // id/name/version/ownsCompaction/turnMaintenanceMode
  bootstrap?(p):  Promise<BootstrapResult>;       // 启动时导入历史
  ingest(p):      Promise<IngestResult>;          // 入一条消息
  ingestBatch?(p):Promise<IngestBatchResult>;     // 入一个 turn 的批
  assemble(p):    Promise<AssembleResult>;        // 装配 model context（按 token budget）
  compact(p):     Promise<CompactResult>;         // 压缩
  afterTurn?(p):  Promise<void>;                  // turn 完成后处理
  maintain?(p):   Promise<ContextEngineMaintenanceResult>; // transcript 重写
  prepareSubagentSpawn?(p): Promise<SubagentSpawnPreparation | undefined>;
  onSubagentEnded?(p): Promise<void>;
  dispose?(): Promise<void>;
}

// 运行时上下文（注入给 engine）
type ContextEngineRuntimeContext = {
  allowDeferredCompactionExecution?: boolean;
  tokenBudget?: number;
  currentTokenCount?: number;
  promptCache?: ContextEnginePromptCacheInfo;      // retention/lastCallUsage/observation
  rewriteTranscriptEntries?: (req) => Promise<TranscriptRewriteResult>;
  llm?: { complete: (params) => Promise<...> };    // 引擎要做 LLM 召回时回调
};
```

### 调用链

```
turn 开始
  ├─ engine.bootstrap?  (首次)
  ├─ engine.assemble({ messages, tokenBudget, availableTools, prompt })
  │     → AssembleResult { messages, estimatedTokens, promptAuthority, systemPromptAddition }
  ├─ LLM call
  ├─ engine.ingest(assistantMessage)
  ├─ engine.afterTurn({ messages, prePromptMessageCount, tokenBudget, runtimeContext })
  └─ engine.compact? (over budget 时)
       └─ rewriteTranscriptEntries (branch-and-reappend，不破坏历史 DAG)
```

### 可借鉴点
- **强烈可借**：`ContextEngine` interface 几乎可以**整套搬到我们 NestJS** 当
  `ContextEngine` provider 接口——多租户场景下不同 tenant 可注册不同引擎
  （短期 in-memory / RAG-aware / summary-based）。
- **强烈可借**：`promptAuthority: "assembled" | "preassembly_may_overflow"`
  这种**显式声明"哪个 estimate 当真"** 的字段，避免上游误把压缩前的尺寸传下去
  ——我们做 token guard 时该照搬。
- **强烈可借**：`rewriteTranscriptEntries(branch-and-reappend)` 抽象——transcript
  当 DAG 看而非 append-only log，**压缩 = 新建分支**，原历史保留。这是契约面，
  我们多租户 agent 历史压缩也该这么搞。
- **可借**：`prompt-cache observation`（`broke / previousCacheRead / changes
  [cacheRetention|model|streamStrategy|systemPrompt|tools|transport]`）这一套
  **可观测**结构——我们也该上报"为什么缓存命中率掉了"。

---

## 1.5 Model Catalog / Provider Runtime / Routing

### 职责
- `model-catalog/`：**多 provider × 多 source 的统一模型目录**——支持 `manifest / provider-index / cache / config / runtime-refresh` 多来源，按 authority 合并。
- `provider-runtime/`：**运行时 provider 通用工具**（当前只剩 `operation-retry.ts`，因为多数 provider 适配现在在 `agents/` 和 `extensions/` 里）。
- `routing/`：**渠道消息 → 内部 session 的路由**——account-id 解析、binding 解析、peer-kind 匹配、resolve-route 主入口。

### 关键文件
- `src/model-catalog/types.ts:1-134`：`ModelCatalog / ModelCatalogModel / ModelCatalogProvider / UnifiedModelCatalogEntry / NormalizedModelCatalogRow`。
- `src/model-catalog/authority.ts:1-32`：source authority（`config < manifest < cache/runtime-refresh < provider-index`，**数值越小优先级越高**）；按 `mergeKey` 合并。
- `src/model-catalog/manifest-planner.ts`、`provider-index-planner.ts`、`normalize.ts`、`refs.ts`：从各 source 解析 → 归一 → planner → 合并。
- `src/routing/resolve-route.ts`：主 entry，把"来自某 channel 的某个 conversation"解析成"内部哪个 session/agent"。
- `src/routing/bindings.ts`、`binding-scope.ts`：**conversation ↔ session 绑定**。
- `src/routing/account-lookup.ts`、`account-id.ts`：channel 账号解析。
- `src/agents/configured-provider-fallback.ts:8-39`：当 default provider/model 不在 user config 里时，回退到第一个配置好的 provider。

### authority 合并规则

```ts
const AUTHORITY = {
  config: 0,              // user 显式配置最高（数值最小=赢）
  manifest: 1,            // 内置 manifest
  cache: 2, "runtime-refresh": 2,
  "provider-index": 3,    // 远程 index 兜底
};
// 同 mergeKey 取 authority 最小的那条
```

### 调用链（路由）

```
channel inbound message
  └─ routing.resolveRoute({ channel, accountRef, peerRef, conversationRef })
       ├─ accountLookup → routing.accountId
       ├─ bindings.resolveByConversation (持久绑定优先)
       ├─ peerKindMatch (DM/Group/Channel 类型匹配)
       └─ channelRouteTargets → { agentId, sessionKey }
```

### 可借鉴点
- **强烈可借**：`source authority` 的**数字越小越赢**这种合并策略——
  我们多租户配置（系统默认 / org 级覆盖 / project 级覆盖 / runtime override）
  完全是同一模式，且 OpenClaw 把它**只用 30 行代码**实现得很干净。
- **强烈可借**：`mergeKey` 抽象——不是按 `(provider, modelId)` 死匹配，
  而是按可配置的合并键，避免别名碰撞。
- **可借**：`routing/` 把"外部 conversation → 内部 session"的解析抽成**纯函数 +
  binding 表**，跟我们多租户"外部 webhook ID → 内部 entity"完全同构。
- **fallback 链不能直接抄**：`configured-provider-fallback.ts` 是 CLI 单用户
  场景的简单实现（"第一个有 models 的就用"）。多租户场景需要按 tenant 配
  fallback 链；但**这个 30 行函数说明 fallback 不必复杂**——优先级列表 +
  `FailoverError.suspend` 熔断就够了。

---

## 1.6 控制面：Gateway / Daemon / Node-host

### 职责
- `gateway/`（**485 个文件，整个仓库最大模块**）：HTTP/WS 服务、auth、plugin HTTP、ws-connection、agent 命令分发、presence、health/readiness、hooks——是"渠道 → agent → 渠道"的中枢。
- `daemon/`：把 gateway 包装成 launchd / systemd / schtasks 系统服务的安装/启停/诊断。
- `node-host/`：宿主 Node 进程的特殊场景（**目录在 src 树中存在但被聚合在其它模块下**，承担 plugin host 的角色）。

### 关键文件
- `src/gateway/server/ws-connection.ts`、`ws-connection/`：WebSocket 长连接（channel 接入主要通道）。
- `src/gateway/server/http-listen.ts`、`tls.ts`、`readiness.ts`、`health-state.ts`、`event-loop-health.ts`：HTTP server 起停 + 健康检查。
- `src/gateway/server/plugins-http.ts`：plugin HTTP routes。
- `src/gateway/server/hooks*.ts`、`hooks-request-handler.ts`：hook 处理。
- `src/gateway/agent-prompt.ts`、`agent-list.ts`、`agent-event-assistant-text.ts`：agent 命令分发。
- `src/gateway/client.ts`、`client-bootstrap.ts`、`client-start-readiness.ts`：gateway 客户端（ACP/MCP server 内部连本地 gateway 用）。
- `src/daemon/service.ts`、`launchd.ts`、`systemd.ts`、`schtasks.ts`：三大 OS 的服务安装器。
- `src/daemon/gateway-entrypoint.ts`：daemon 模式 gateway 入口。

### 可借鉴点
- **可借**：gateway 把"传输（WS/HTTP）/认证/路由/hook/health"严格分文件——
  我们 NestJS 已经天然分 Controller/Guard/Interceptor，但**hook 体系**
  （在请求前后注入业务方处理）值得抄。
- **可借**：`event-loop-health.ts` 这种**进程内 event loop lag 监测** + readiness
  endpoint 的做法——多租户 web agent 长时间跑 LLM 时，event loop 可能阻塞，
  health 检查不能只看 HTTP 200。
- **不抄**：daemon 安装这一套是给桌面 CLI 用的，我们 web 端走 k8s/PM2 不需要。

---

## 1.7 ACP 与 MCP：双向协议桥

ACP 和 MCP 在 OpenClaw 里**方向相反**：

| 协议 | 方向 | OpenClaw 在协议中的角色 | 用途 |
| --- | --- | --- | --- |
| **ACP** (Agent Client Protocol) | client → OpenClaw | **server**（agent） | 把 OpenClaw 暴露成一个标准 ACP server，让 Zed 等 ACP client 把它当 agent 用 |
| **MCP** (Model Context Protocol) | OpenClaw → CLI/IDE | **server**（tool 提供方） | 把 OpenClaw 自己的渠道工具（send_message / list_conversations 等）暴露给 Claude/Codex/Gemini 等 MCP client 调用 |

### ACP（`src/acp/`）
- `src/acp/server.ts:22`：`serveAcpGateway(opts)`——主入口，跑 ACP server。
- `src/acp/translator.ts:1-60`：**核心翻译层**——把 ACP 的 `Initialize/NewSession/Prompt/LoadSession/SetSessionMode/Cancel` 等请求翻译成 gateway client 调用。
- `src/acp/session.ts`、`session-mapper.ts`、`session-lineage-meta.ts`：ACP sessionId ↔ OpenClaw sessionKey 双向映射。
- `src/acp/event-ledger.ts`、`event-mapper.ts`：**事件账本**——ACP 必须保证事件 replay 一致性，所以单独有 ledger（schema 在 `event-ledger.ts`）。
- `src/acp/permission-relay.ts`、`approval-classifier.ts`、`policy.ts`：**权限/审批转发**——agent 想 run bash？ACP 把请求转给 ACP client（IDE）确认，再转回来。
- `src/acp/persistent-bindings.*`：ACP 会话 → 持久 channel binding 的桥。
- `src/acp/types.ts:21-53`：`AcpSession / AcpServerOptions / ACP_AGENT_INFO`。

### MCP（`src/mcp/`）
- `src/mcp/channel-server.ts:28-71`：`createOpenClawChannelMcpServer`——用 `@modelcontextprotocol/sdk/server/mcp.js` 起 MCP server，注册 `ClaudePermissionRequestSchema` notification handler 和 channel tools。
- `src/mcp/channel-bridge.ts`：**bridge**——把 MCP tool call 翻译成 gateway client 调用。
- `src/mcp/channel-tools.ts`、`channel-shared.ts`：tool schema 注册。
- `src/mcp/openclaw-tools-serve.ts`、`plugin-tools-serve.ts`、`tools-stdio-server.ts`：tool stdio 服务变体。
- `src/agents/bundle-mcp*.ts`、`agents/cli-runner/bundle-mcp-*.ts`：**bundle MCP**——把 channel MCP server 序列化进 Claude/Codex/Gemini 的启动配置，子 CLI 启动时自动连上。

### 可借鉴点
- **强烈可借**：ACP/MCP **双向都做**的架构思路——我们 web agent 也可以
  既"被 IDE 当 agent 调"（ACP server），也"把自己的多租户工具暴露给外部 LLM"
  （MCP server）。
- **强烈可借**：`acp/event-ledger.ts` 的**事件账本 + replay**模式——ACP client
  断线重连后需要重放未消费事件。我们 SSE/WS 长连接 agent 也需要这层，**不要
  让每次断线丢消息**。
- **强烈可借**：`acp/permission-relay.ts` 把"agent 想做什么 → 转发给 client 审批
  → 拿回结果"做成纯转发，**审批策略不放在 agent 侧**——多租户场景里
  approval policy 应当在 client/UI 侧定义，agent 只负责 relay。
- **可借**：`mcp/channel-bridge.ts` 这种 "MCP tool → 内部 service" 的 thin
  adapter 模式——我们 NestJS 暴露 MCP 时，**只暴露 controller 层的 tool schema +
  delegate 到 service**，不要在 MCP handler 里写业务。

---

## 1.8 不熟概念：Bindings / Commitments / Crestodian

### Bindings（`src/bindings/`，仅 1 文件 `records.ts`）

**定义**：一条"外部 channel conversation ↔ 内部 session"的**持久映射记录**。
让某个 Discord 频道/某个手机号 thread 永久指向同一个 agent session，跨重启保留。

- `src/bindings/records.ts:12-48`：5 个纯函数包装（`create / list / resolve /
  touch / unbind`），实际实现委托给 `infra/outbound/session-binding-service.ts`。
- 实际服务定义在 `src/infra/outbound/session-binding-service.ts`：
  ```ts
  type SessionBindingService = {
    bind: (input) => Promise<SessionBindingRecord>;
    getCapabilities: ({ channel, accountId }) => SessionBindingCapabilities;
    listBySession: (targetSessionKey) => SessionBindingRecord[];
    resolveByConversation: (ref: ConversationRef) => SessionBindingRecord | null;
    touch: (bindingId, at?) => void;
    unbind: (input) => Promise<SessionBindingRecord[]>;
  };
  ```

**可借鉴**：我们多租户 web agent 也需要"前端 conversationId ↔ 后端 sessionId" 的持久映射——
这个 `SessionBindingService` 接口可以直接搬：bind / resolveByConversation / touch / unbind +
capabilities，5 个动词够用。

### Commitments（`src/commitments/`）

**定义**：**agent 向用户做出的"未来承诺"** 的状态机持久化。
比如用户说"明天 9 点提醒我开会"，agent 答"好"，OpenClaw **自动抽取出一条 commitment**，到时间自动通过 channel 主动发消息。

- `src/commitments/types.ts:1-93`：`CommitmentRecord` 的完整 schema（**重要参考**）。
  - `kind: "event_check_in" | "deadline_check" | "care_check_in" | "open_loop"`
  - `sensitivity: "routine" | "personal" | "care"`
  - `source: "inferred_user_context" | "agent_promise"`
  - `status: "pending" | "sent" | "dismissed" | "snoozed" | "expired"`——完整状态机
  - `dueWindow: { earliestMs, latestMs, timezone }`——时间窗而非时间点
  - `dedupeKey`、`confidence`、`attempts`、`lastAttemptAtMs`——重试与去重
- `src/commitments/extraction.ts`：**用 LLM 二次抽取**——每个 turn 完后送给一个抽取 prompt，找出 candidate commitments。
- `src/commitments/runtime.ts:34-50`：抽取队列 + 定时器，延迟批量调 LLM。
- `src/commitments/store.ts`：持久化（JSON 文件 v1）。
- `src/commitments/extraction.test.ts`、`commitments-full-chain.integration.test.ts`、`commitments-heartbeat-policy.e2e.test.ts`：单测+集成+e2e 三层覆盖。

**可借鉴**：
- **强烈可借**：`CommitmentRecord` 的 schema——`dueWindow`（窗而非点）/ `sensitivity`（routine/personal/care 决定能不能打扰）/ `dedupeKey`（避免同事件 N 次提醒）/ `confidence`（低于阈值不发）这几个字段我们做"主动通知/follow-up"功能时直接抄。
- **强烈可借**：**用 LLM 二次抽取"承诺"** 这个产品形态——传统 agent 都是被动响应，
  OpenClaw 主动从对话里挖出"我应该后续主动联系用户"的事件。我们多租户 CRM
  类 agent 应该直接抄。
- **强烈可借**：把抽取做成 **deferred queue + timer**，不在主 turn 流程里同步跑——避免阻塞主响应。

### Crestodian（`src/crestodian/`）

**定义**："**custodian + restoration**" 词根杂交——一个**专门负责故障救援的辅助 agent**。
当主 agent 失败/被熔断/账号挂了，crestodian agent 通过备用 channel（备用账号/SMS）联系用户说明情况并尝试恢复。

- `src/crestodian/crestodian.ts:62-100`：`runCrestodian`——主入口，可 interactive TUI 或 one-shot `--message`。
- `src/crestodian/assistant.ts`、`assistant-backends.ts`、`assistant-prompts.ts`：crestodian 自己的 LLM 后端（**独立于主 agent**，避免主 agent 故障时连救援都挂）。
- `src/crestodian/operations.ts`、`dialogue.ts`：crestodian 能做的操作（其中 `isPersistentCrestodianOperation` 决定要不要 `--yes` 才能跑）。
- `src/crestodian/rescue-channel.live.test.ts`、`rescue-message.ts`、`rescue-policy.ts`：**rescue 通道**——
  当主 channel 挂掉时通过备用 channel 发"我挂了"。
- `src/crestodian/audit.ts`、`probes.ts`：审计 + 健康探针。
- `src/crestodian/overview.ts`：跑前给用户看的"当前系统状态总览"。

**可借鉴**：
- **可借**：**故障兜底的"second-line agent"** 这个产品思路——
  我们多租户场景下，主 agent 因为 quota/auth/网络挂了，**不能让用户什么都不知道**。
  应该有一个最简单的 fallback agent（用本地小模型甚至模板）告知用户"主 agent 暂时不可用"并提供基本 self-service。
- **可借**：crestodian 跑前先 `loadCrestodianOverview` 让用户看现状——
  我们做 ops/admin 工具时也该有这种"操作前先 print overview"的强制确认 UX。
- **不抄**：crestodian 的具体实现强依赖多 channel（备用 SMS/邮件），web-only agent 暂时不需要。

---

## 1.9 关键问题汇总

**Q1: agent 主循环跟 Claude Code master-loop 的差异？**
OpenClaw **不自己跑** master-loop。它把 Claude/Codex/Gemini CLI 当**子进程后端**拉
起（`cli-runner` 模块），自己只负责**外围**（prompt 注入、context 守护、session
路由、failover、hook、observability）。Claude Code 是端到端 agent；OpenClaw 是
**agent 的"外壳/编排器"**。这种"宿主成熟 agent"思路对我们 web 端**不适用**——
我们应该直接用 SDK 自跑 loop。但**外围那一套**（FailoverError、auth-profile 池、
hook、enqueue per-session）100% 值得抄。

**Q2: 六个子系统的边界？**
chat=View / flows=Wizard / trajectory=Trace / tasks=Job / status=Notify / talk=Voice。
**不重叠**，记口诀即可。

**Q3: model-catalog 怎么管理多 provider？fallback 怎么实现？**
catalog 用 **source authority 数值合并**（config 0 < manifest 1 < cache 2 <
provider-index 3，**数小赢**），按 `mergeKey` 去重。fallback 极简：
`configured-provider-fallback.ts` 只有 30 行——default provider 不可用就取第一个
有配置的；真正的"运行时切换"靠 `FailoverError.suspend` 把当前 profile 熔断后由
auth-profile 池给下一个 active profile。

**Q4: ACP 和 MCP 分别充当什么？**
方向相反：**ACP server** = 把 OpenClaw 当 agent 暴露给 ACP client（如 Zed IDE）；
**MCP server** = 把 OpenClaw 的渠道工具（发消息/查会话）暴露给 MCP client
（Claude/Codex/Gemini CLI）。两边都做才能"在 IDE 里调 OpenClaw 当 agent，且
OpenClaw 内部又能调多个 CLI agent 当工具"。

**Q5: bindings / commitments / crestodian？**
- **bindings**：persistent "外部 conversation ↔ 内部 session" 映射记录。
- **commitments**：agent 向用户做出的未来承诺的**状态机持久化**（pending→sent/dismissed/snoozed/expired），由 LLM 二次抽取生成，定时主动触发。
- **crestodian**：主 agent 挂掉时的**救援 agent**，通过备用 channel 通知用户并协助恢复。

---

## 1.10 总结：可直接抄进我们 NestJS web agent 的 7 个点

1. **`ContextEngine` interface**（`context-engine/types.ts:190+`）——整套搬。
2. **`FailoverError` 结构化错误协议**（`agents/failover-error.ts:16-60`）——多 provider 适配的统一错误。
3. **`SessionBindingService` 6 个动词接口**（`bindings/records.ts` + `infra/outbound/session-binding-service.ts`）——外部 conversationId ↔ 内部 sessionId。
4. **`CommitmentRecord` schema + 二次抽取 deferred queue**（`commitments/types.ts:1-93` + `commitments/runtime.ts`）——主动 follow-up 能力。
5. **`TrajectoryEvent` schema**（`trajectory/types.ts:9-28`）——多租户调试追溯。
6. **`source authority` 数字合并**（`model-catalog/authority.ts:1-32`）——多层配置覆盖（系统/org/project/runtime）。
7. **ACP `event-ledger` + replay**（`acp/event-ledger.ts`）——长连接断线重连不丢事件。

**慎抄**：cli-runner（spawn 外部 CLI）、daemon（launchd/systemd 安装）、entry
respawn/compile-cache（Node CLI 启动优化）这三类是 OpenClaw 的桌面 CLI 形态特
有的，跟 web 服务架构不对路。


---

# 二、渠道与消息

> 研究目标版本：**openclaw `v2026.5.14`**，路径 `/tmp/openclaw-fresh/`。
> 本章覆盖 `src/channels/`、`src/pairing/`、`src/auto-reply/`、`src/polls.ts` 以及 `extensions/` 中所有 IM 渠道扩展的**抽象层 + 注册流程**。具体渠道实现（discord/telegram/slack 等）只看顶层入口与 manifest，不逐文件展开。

---

## 0. 全景速记（先看这段）

OpenClaw 把"对接任意 IM"这件事拆成五段流水线：

```
[Channel Extension]    plugin registry          [Channels Core]              [Auto-Reply / Agent]
 IM SDK / Webhook  ──▶ ChannelPlugin              ──▶ Channel Turn Kernel ──▶ dispatchReplyFromConfig
   (inbound raw)       .messaging / .send /            (9-stage state machine)     (model + tools)
                       .receive / .messageActions
                       .pairing / .security
                                                                                       │
                                                                                       ▼
[Outbound Adapter] ◀── ChannelTurnDeliveryAdapter ◀── reply pipeline ◀── ReplyPayload
 channel-id-keyed       (durable + live preview)        (chunk + sanitize)
 sendText / sendMedia
 sendPayload / chunker
```

关键设计点：
- **一个 ChannelPlugin = 一个"渠道契约 + 多个 adapter"集合**（types.core.ts 把 messaging / threading / mention / security / messageActions / outbound / pairing 全部拆成独立 adapter，不强制混在一起）。
- **入站归一化不在固定 `normalize/` 目录**——v2026.5.14 在 plugins 里没有独立 `normalize/` 子目录，取而代之的是 `ChannelTurnAdapter.ingest(raw) → NormalizedTurnInput` 这条统一管线，加上 `ChannelMessagingAdapter.resolveSessionConversation / parseExplicitTarget / normalizeTarget` 等"按需归一化"hook。
- **出站统一走 `ChannelOutboundAdapter`**，由 `plugins/outbound/load.ts` 按 `ChannelId` 懒加载，`direct-text-media.ts` 提供"chunk + sanitize + sendText/sendMedia/sendPayload"通用工厂；`registry-loader.ts` 提供按 id 懒加载的 ESM 注册表。
- **`auto-reply/` 是 inbound 处理核心**——名字误导，实际包含命令检测 / 心跳 / 节流 / 模型 dispatch / 块流式 reply 等"从消息到 agent 回复"的全部主流程，不是"被动自动回复"的小工具。
- **`pairing/` 是 DM 准入闸门**——首次 DM 不放进 agent，先发"配对码 + ID 行"挑战；用户在 setup CLI/UI 输入码后写入 `allow-from-*.json`，下条消息直通。
- **`polls.ts` ≠ 消息轮询**：是"群投票/poll 卡片"的输入归一化器（question + options + duration），由各渠道按能力下发为原生投票。

确认结论：
- `src/web/` 在最新版只有 `provider-runtime-shared.ts`，是 LLM provider runtime 工具，**不是** WhatsApp Web QR。
- `src/channels/` 下没有名为 `web` / `telegram` 的子目录（旧版的 `channels/web/` 已下沉到 `extensions/whatsapp/`）。
- `src/channels/plugins/` 下**没有** `normalize/` / `onboarding/` / `agent-tools/` 这三个子目录——这些职责被吸收到 `types.core.ts` 的 adapter 集合 + `turn/kernel.ts` 的状态机中。

v2026.5.14 实际存在的渠道 extension（`ls extensions/`，过滤 IM 类）：
```
device-pair  discord  feishu  googlechat  imessage  irc  line  matrix
mattermost   msteams  nextcloud-talk  nostr  qa-channel  qa-matrix
signal  slack  synology-chat  telegram  tlon  twitch  webhooks  whatsapp
zalo  zalouser
```
（`bluebubbles` / `m365-tools` 在 v2026.5.14 **不存在**为独立顶级目录；bluebubbles 已并入 `imessage` 的 BlueBubbles 后端，m365 工具应该并入 `msteams` 或 m365 工具集。）

---

## 1. `src/channels/`：渠道核心抽象

### 1.1 职责
- 定义"什么是一个 channel"的所有类型（`ChannelPlugin`、`ChannelMessagingAdapter`、`ChannelOutboundAdapter`、`ChannelMessageActionAdapter` 等 30+ adapter 接口）。
- 维护**两级注册表**：bundled（编译期 hardcoded id 列表）+ loaded（运行期 plugin 加载）。
- 提供 channel 无关的"消息生命周期"（receive ack、send durable、live preview/finalize、receipt、reply pipeline）。
- 提供 channel turn kernel（一条入站消息从 raw 到 agent dispatch 的 9-stage 状态机）。
- 提供 allowlists / DM 准入 / mention gating / typing / status-issue / read-only inspect 等横切能力。

### 1.2 关键文件

| Path | 行 | 作用 |
|---|---|---|
| `src/channels/registry.ts:1-64` | 64 | 极薄的 ID 归一化入口；故意不 import plugin 代码以避免冷启动拉满 SDK |
| `src/channels/plugins/types.core.ts:1-790` | 790 | **本章核心**：定义全部 adapter 接口。`ChannelMeta` / `ChannelCapabilities` / `ChannelMessagingAdapter` / `ChannelMentionAdapter` / `ChannelThreadingAdapter` / `ChannelMessageActionAdapter` / `ChannelAgentTool` / `ChannelStatusIssue` / `ChannelHeartbeatDeps` / `ChannelSecurityDmPolicy` / `ChannelOutboundSessionRoute` |
| `src/channels/plugins/types.plugin.ts` | - | `ChannelPlugin` 顶层聚合类型 |
| `src/channels/plugins/registry.ts:1-43` | 43 | 双层查找：loaded（runtime）→ bundled（fallback） |
| `src/channels/plugins/registry-loader.ts` | - | 按 ChannelId 懒加载某个 adapter 字段的工厂 |
| `src/channels/plugins/bundled.ts` | - | 编译期已知 channel id 集合（核心常量） |
| `src/channels/plugins/module-loader.ts` | - | ESM dynamic-import 包装，处理 origin / version skew |
| `src/channels/plugins/outbound/load.ts:1-21` | 21 | `loadChannelOutboundAdapter(id)`：出站投递的懒加载主入口 |
| `src/channels/plugins/outbound/direct-text-media.ts:1-157` | 157 | **复用利器**：`createDirectTextMediaOutbound(...)` 工厂，给"原生 sendText / sendMedia"型渠道（discord/telegram/whatsapp）一行造一个 ChannelOutboundAdapter |
| `src/channels/plugins/outbound/interactive.ts:1-13` | 13 | `reduceInteractiveReply()`：把 InteractiveReply 的 blocks 归约成渠道原生组件 |
| `src/channels/plugins/pairing.ts` / `pairing-adapters.ts` | - | `ChannelPairingAdapter` 协议 + 跨渠道的 allow-from 文件适配 |
| `src/channels/plugins/setup-wizard*.ts` | - | onboarding CLI/TUI（即题目说的"onboarding"职责，现在不是子目录而是顶层文件群） |
| `src/channels/plugins/status-issues/shared.ts` | - | `ChannelStatusIssue` 的渲染共享逻辑（auth / config / runtime 三类问题） |
| `src/channels/plugins/contracts/inbound-testkit.ts` | - | 入站事件 testkit；plugin 共享的 fixture |
| `src/channels/plugins/contracts/outbound-payload-testkit.ts` | - | 出站 payload 契约 testkit |
| `src/channels/plugins/contracts/*-shard-{a..h}.contract.test.ts` | - | **8 个 shard 的注册表契约测试**：每加一个 plugin 必须过这套，保证 registry 形状统一 |
| `src/channels/message/types.ts:1-368` | 368 | 消息生命周期类型：`MessageReceipt` / `RenderedMessageBatch` / `LiveMessageState` / `MessageSendContext` / `ChannelMessageSendAdapter` / `DurableFinalDeliveryRequirementMap` |
| `src/channels/message/receive.ts:1-86` | 86 | **ack 状态机**：`receive_record` / `agent_dispatch` / `durable_send` / `manual` 四档 ack 时机 |
| `src/channels/message/send.ts` | 349 | 发送侧 helpers（chunk + retry + receipt 装配） |
| `src/channels/message/live.ts` | - | "草稿预览 → 最终化"实时消息（draftPreview / previewFinalization） |
| `src/channels/message/outbound-bridge.ts` | - | 把 ReplyPayload 桥到 ChannelOutboundAdapter |
| `src/channels/message/receipt.ts` | - | MessageReceipt 装配与合并 |
| `src/channels/message/reply-pipeline.ts` | - | chunk + sanitize + buffered block dispatcher |
| `src/channels/turn/types.ts:1-443` | 443 | **Turn 状态机的契约**：9 stages（ingest/classify/preflight/resolve/authorize/assemble/record/dispatch/finalize）+ admission 四态（dispatch/observeOnly/handled/drop） |
| `src/channels/turn/kernel.ts:1-645` | 645 | Turn 状态机执行体；从 raw → preflight → resolve → assemble → record → dispatch → finalize |
| `src/channels/turn/bot-loop-protection.ts` | - | bot 互相回复死循环检测（pair 维度抑制） |
| `src/channels/turn/durable-delivery.ts` | - | 可恢复投递：`reconcileUnknownSend` 用于"我送出去了但没拿到 receipt"场景 |
| `src/channels/transport/stall-watchdog.ts` | - | inbound transport 卡住检测（>N 秒无心跳就标 unhealthy） |
| `src/channels/allowlists/resolve-utils.ts` | - | allowlist 通用解析 |
| `src/channels/message-access/runtime.ts` | - | DM/group 访问决策 runtime |
| `src/channels/session.ts` / `session-envelope.ts` | - | session key / envelope 抽象（agent 维度的会话边界） |
| `src/channels/ids.ts` | - | 内置 ChatChannelId 排序 + 归一化 |
| `src/channels/chat-type.ts` | - | direct / group / channel / thread 枚举 |

### 1.3 数据结构（核心几个）

**ChannelPlugin**（聚合根，简化版）：
```ts
type ChannelPlugin = {
  id: ChannelId;
  meta: ChannelMeta;                    // 给 picker/docs 用的元信息
  capabilities: ChannelCapabilities;    // polls/reactions/edit/threads/media/tts/nativeCommands...
  outbound?: ChannelOutboundAdapter;    // 出站
  message?: ChannelMessageAdapter;      // 消息生命周期（send/receive/live/durableFinal）
  messaging?: ChannelMessagingAdapter;  // target/session/thread 解析
  threading?: ChannelThreadingAdapter;
  mention?: ChannelMentionAdapter;
  security?: { dmPolicy?: ChannelSecurityDmPolicy; ... };
  messageActions?: ChannelMessageActionAdapter;  // 暴露给 agent 的 message tool action
  agentPrompt?: ChannelAgentPromptAdapter;       // 给 system prompt 注入 channel-specific hints
  pairing?: ChannelPairingAdapter;
  agentTools?: ChannelAgentTool[] | ChannelAgentToolFactory;
  setup?: ChannelSetupAdapter;
  // ...十几个可选 adapter，全部互不强耦合
};
```

**ChannelTurnAdapter**（types.ts:401-419）：
```ts
type ChannelTurnAdapter<TRaw, TDispatchResult> = {
  ingest: (raw: TRaw) => NormalizedTurnInput | null;              // 归一化
  classify?: (input) => ChannelEventClass;                        // message/command/reaction/lifecycle
  preflight?: (input, eventClass) => PreflightFacts | ChannelTurnAdmission;
  resolveTurn: (input, eventClass, preflight) => ChannelTurnResolved;
  onFinalize?: (result) => void;
};
```

**ChannelTurnAdmission**（types.ts:22-26）：四态准入裁决
```ts
| { kind: "dispatch"; reason? }       // 喂给 agent
| { kind: "observeOnly"; reason }     // 记历史不回复（典型：bot 自己说话）
| { kind: "handled"; reason }         // 已被 channel 自己消化（典型：pairing 挑战、slash command）
| { kind: "drop"; reason; recordHistory? }
```

**MessageReceipt**（message/types.ts:61-71）：
```ts
type MessageReceipt = {
  primaryPlatformMessageId?: string;
  platformMessageIds: string[];
  parts: MessageReceiptPart[];      // 一次回复可能是多条（文本+图+卡片）
  threadId?: string;
  replyToId?: string;
  editToken?: string;               // 用于"草稿预览 → 最终编辑"
  deleteToken?: string;
  sentAt: number;
  raw?: readonly MessageReceiptSourceResult[];
};
```

**ack 策略**（message/receive.ts）：四档可选
- `after_receive_record`：写完 history 就 ack（默认；不卡 agent）
- `after_agent_dispatch`：agent 收到了再 ack（弱保证）
- `after_durable_send`：回复真正出去了才 ack（强保证，用于 webhook 队列）
- `manual`：channel 自己控制

### 1.4 调用链（一条入站消息的完整旅程）

```
1. extensions/<channel>/runtime-api.*.ts          IM SDK / webhook 收到原始事件
                          │  raw event
                          ▼
2. channel-side runTurn(raw, adapter)              新代码用 `runChannelTurn` 包一层
                          │
                          ▼
3. ChannelTurnAdapter.ingest(raw)  ─ NormalizedTurnInput { id, rawText, textForAgent, textForCommands }
                          │
                          ▼
4. ChannelTurnAdapter.classify(input)  ─ ChannelEventClass { kind: "message"|"command"|..., canStartAgentTurn }
                          │
                          ▼
5. ChannelTurnAdapter.preflight(...)  ─ PreflightFacts | ChannelTurnAdmission
   关键检查：DM allowlist / group policy / mention gating / bot loop / sender role
   命中 admission=handled  → 走 pairing 挑战或 slash command，直接退出
   命中 admission=drop     → 静默丢弃
   命中 admission=observeOnly → 只记历史不回复
                          │
                          ▼
6. ChannelTurnAdapter.resolveTurn(...)  → ChannelTurnResolved
   组装 sessionKey、agentId、threadId、ReplyPlanFacts、SupplementalContextFacts（quote/forward/thread）
   resolveSessionConversation 拆 thread/topic 维度
                          │
                          ▼
7. record（session metadata + inbound history） + ack（按 ackPolicy）
                          │
                          ▼
8. dispatchReplyFromConfig（auto-reply/dispatch.ts）
   - foregroundReplyFence（同 session 串行）
   - withReplyDispatcher → createReplyDispatcherWithTyping
   - dispatchReplyWithBufferedBlockDispatcher
   - 调 LLM (provider runtime)，流式产出 ReplyPayload
                          │
                          ▼
9. ChannelTurnDeliveryAdapter.deliver(payload, info)
   - 走 createChannelReplyPipeline：chunk + sanitize + reply-to 解析
   - 调 ChannelOutboundAdapter.sendText / sendMedia / sendPayload
   - durable=true 时进 outbound queue（reconcileUnknownSend 兜底）
                          │
                          ▼
10. onFinalize(result)   清 history、写 receipt、可选 ack
```

### 1.5 可借鉴点（给我们自研 Agent 内核）

1. **Channel 接口拆 30 个小 adapter 而不是一个大 interface**。每个 adapter 都可空、可继承默认实现，新接渠道只填自己关心的几个（discord 14 个、telegram 16 个），心智负担可控。我们 FFOA 后端的"IM 适配层"如果要做，建议照抄"adapter-per-concern"分解，**禁止**把"接 X IM"做成一个 god class。
2. **Turn 状态机的 admission 四态**（dispatch/observeOnly/handled/drop）是关键抽象。一条消息不是"要么处理要么忽略"——`handled`（已被渠道自己消化掉，比如 pairing/slash）和 `observeOnly`（要记上下文但不回）是两个常被混淆的中间态，分开后调用链非常干净。
3. **9-stage 状态机 + Per-stage log event**（types.ts:361-382 `ChannelTurnStage`）。每个阶段都有 `start/done/drop/handled/error` 五个事件，问题排查直接看 timeline 即可。值得借鉴到我们的 Temporal workflow 排查上。
4. **MessageReceipt 多 part**：一次"回复"在 IM 里可能是 5 条独立消息（开场文本 + 图 + 引用 + 卡片 + 收尾），用 `parts[]` 而非单 messageId 表达，后续 edit/delete 才有锚点。我们做客户群消息时如果要支持"修改已发"，必须把 receipt 设计成 parts。
5. **ack 四档策略**：尤其 `after_durable_send`（确认真正落到 IM 之后才 ack 上游 webhook）这条，是处理 Webhook + 队列 + 重试时的关键。我们日常 webhook 处理只做了"收到立刻 200"，遇到下游失败就丢消息——抄这个分档。
6. **plugin 注册的 8-shard 契约测试**（`contracts/*-shard-{a..h}.contract.test.ts`）：保证所有 plugin 形状一致、缺字段在 CI 卡住。等价于我们的 L0a/L0b。
7. **`createDirectTextMediaOutbound(...)` 工厂**：把"原生有 sendText/sendMedia 的渠道"一句话造出 ChannelOutboundAdapter，强烈值得在我们自研 IM SDK 抽象里复刻。
8. **`reconcileUnknownSend`**（durable-delivery.ts）：网络抖动后查回执，决定 retry 还是 commit。我们做出站消息可靠投递时这是必须的兜底。

---

## 2. `src/channels/plugins/`：plugin 注册与 adapter 契约

### 2.1 职责
作为 src/channels/ 的下层，专门负责：
- 把"渠道 plugin"按 ChannelId 注册进运行时 registry
- 区分 bundled（编译期/源码自带）vs loaded（runtime/external）
- 每类 adapter 都有独立的 lazy loader（避免冷启动加载所有渠道）
- 提供 setup-wizard（题目说的"onboarding"，但**不是 normalize 子目录**——是顶层文件群 `setup-wizard*.ts` / `setup-helpers*.ts` / `setup-registry.ts`）

### 2.2 关键文件

| Path | 行 | 作用 |
|---|---|---|
| `src/channels/plugins/index.ts:1-26` | 26 | barrel 导出（核心 26 行） |
| `src/channels/plugins/registry.ts:1-43` | 43 | `getChannelPlugin(id)`：loaded > bundled fallback |
| `src/channels/plugins/registry-loader.ts` | - | `createChannelRegistryLoader<T>((entry) => entry.plugin.X)`：按字段懒加载工厂 |
| `src/channels/plugins/bundled.ts` | - | bundled id 常量列表（用于 cold path 防御） |
| `src/channels/plugins/module-loader.ts` | - | dynamic import wrapper |
| `src/channels/plugins/bootstrap-registry.ts` | - | 启动期把 extensions/ 里的 channel manifest 注入 registry |
| `src/channels/plugins/setup-wizard.ts` | - | TUI/CLI 配对向导主入口 |
| `src/channels/plugins/setup-registry.ts` | - | 配对向导可识别的渠道 id 列表 |
| `src/channels/plugins/legacy-config.ts` | - | 兼容旧配置 schema |
| `src/channels/plugins/configured-binding-*.ts` | - | 配置驱动的 thread-binding（把"#general 锁定到 agent X"做成声明式） |
| `src/channels/plugins/binding-routing.ts` | - | inbound → agent route 解析 |
| `src/channels/plugins/directory-adapters.ts` | - | 用户/群目录解析（@xxx → id） |
| `src/channels/plugins/exec-approval-local.ts` | - | 工具调用前的"用户授权"提示（命令执行型 tool 在 IM 里弹给用户点确认） |
| `src/channels/plugins/native-approval-prompt.ts` | - | 渠道原生组件（discord button / slack block）的授权提示 |

### 2.3 数据结构

**ChannelOutboundAdapter**（types.adapters.ts，由 direct-text-media.ts 构造）：
```ts
type ChannelOutboundAdapter = {
  deliveryMode: "direct" | "queued";
  chunker: (text, limit) => string[];
  chunkerMode: "text" | "markdown";
  textChunkLimit: number;
  sanitizeText: (params) => string;
  sendText:    (ctx: ChannelMessageSendTextContext)    => Promise<ChannelMessageSendResult>;
  sendMedia:   (ctx: ChannelMessageSendMediaContext)   => Promise<ChannelMessageSendResult>;
  sendPayload: (ctx: ChannelMessageSendPayloadContext) => Promise<ChannelMessageSendResult>;
};
```

**ChannelMessageActionAdapter**（types.core.ts:712-757）：
渠道把自己想暴露给 agent 的 message tool action 通过 `describeMessageTool` 自描述：
```ts
type ChannelMessageActionAdapter = {
  describeMessageTool(ctx): {
    actions?: ChannelMessageActionName[];        // send / edit / delete / react / poll / ...
    capabilities?: ChannelMessageCapability[];
    schema?: { properties, actions?, visibility? };
    mediaSourceParams?: ...;
  };
  prepareSendPayload?(ctx): ReplyPayload | null;  // 把 agent 的 message(action=send) 调用翻译成 durable ReplyPayload
  handleAction?(ctx): Promise<AgentToolResult>;   // 自定义 action 落地
};
```
这是渠道**自描述给 LLM 的核心**：agent 看到的 `message` tool schema 是各 channel adapter 拼出来的，不是硬编码的。

### 2.4 调用链（注册流程）

```
启动期：
  bootstrap-registry.ts
    → 扫描 extensions/*/openclaw.plugin.json
    → 每个 manifest 声明 channels: ["discord"], channelEnvVars: {...}
    → defineBundledChannelEntry({ id, plugin: {specifier, exportName}, runtime: {...}, registerFull(api) })
    → 加进 loaded registry，但**不立刻 import plugin code**（specifier 留着 lazy）

运行期某个 ChannelId 第一次被用到：
  getChannelPlugin("discord")
    → getLoadedChannelPluginById("discord")
    → module-loader 按 specifier 动态 import
    → 拿到 discordPlugin: ChannelPlugin
    → 后续按 adapter 字段分别懒加载（outbound/load.ts、registry-loader.ts）
```

### 2.5 可借鉴点

1. **adapter 分字段 lazy load**：discord plugin 完整 dependency 树超大（gateway/intents/voice/components），但 outbound 只需要 send fns。`createChannelRegistryLoader<ChannelOutboundAdapter>((entry) => entry.plugin.outbound)` 这种"按字段懒加载"模式比"按 plugin 懒加载"细一档，冷启动收益明显。我们 FFOA 后端的模块加载也可以借鉴。
2. **manifest 驱动注册** + **specifier/exportName 声明式 import**：避免在 core 写死 `import './discord.js'`。多租户场景下，租户可以只激活自己买的渠道扩展。
3. **`describeMessageTool` 渠道自描述**：与其在 core 写"什么渠道有哪些 action"的大 switch，不如让渠道自己描述并由 core 拼装 tool schema。我们做"agent 看到什么工具"时强烈推荐这种 plugin self-description。
4. **`configured-binding-*`：配置驱动绑定**：声明式说"#general 这个 channel 永远路由到 agent X"，不要在 if/else 里写死。
5. **`exec-approval-local.ts` + `native-approval-prompt.ts`**：工具调用前在 IM 里弹原生确认（discord button / slack block / iMessage choice），用户点了才执行。这是把"危险工具"接到 IM 时的标配 UX 模式。

---

## 3. `src/channels/turn/`：Turn 状态机内核

### 3.1 职责
一条入站消息从 raw 到 agent dispatch、再到出站投递的**完整生命周期编排器**。是 channels/ 里最"重"的子系统（kernel.ts 645 行）。

### 3.2 关键文件

| Path | 行 | 作用 |
|---|---|---|
| `src/channels/turn/kernel.ts:1-645` | 645 | `runChannelTurn` 主循环 + `runResolvedChannelTurn` 已 resolve 入口；assemble/record/dispatch/finalize |
| `src/channels/turn/types.ts:1-443` | 443 | 9-stage + admission 4态 + 全部 Facts 类型（Sender/Conversation/Route/ReplyPlan/Access/Message/Supplemental/InboundMedia） |
| `src/channels/turn/context.ts` | - | `buildChannelTurnContext` 把 Facts 装配成 MsgContext |
| `src/channels/turn/bot-loop-protection.ts` | - | bot pair (botA→botB→botA) 抑制 |
| `src/channels/turn/durable-delivery.ts` | - | `deliverInboundReplyWithMessageSendContext` + `reconcileUnknownSend` |
| `src/channels/turn/delivery-result.ts` | - | `createChannelDeliveryResultFromReceipt` |
| `src/channels/turn/dispatch-result.ts` | - | 计数器（textCount / mediaCount / interactiveCount） |

### 3.3 数据结构（Facts 家族）

Turn 状态机的核心抽象是把"一条消息的所有 context"拆成多个独立 Facts 对象，preflight 产出 `PreflightFacts`（admission/message/media/supplemental），resolveTurn 产出 `AssembledChannelTurn`（route/conversation/agent/replyPipeline/delivery 全部装好）。

7 个 Facts：
- `SenderFacts` ─ id/name/username/roles/isBot/isSelf
- `ConversationFacts` ─ kind/id/threadId/nativeChannelId/parentId/routePeer
- `RouteFacts` ─ agentId/accountId/routeSessionKey/dispatchSessionKey
- `ReplyPlanFacts` ─ to/replyToId/threadId/sourceReplyDeliveryMode
- `AccessFacts` ─ dm/group/commands/event/mentions 各自一份决策
- `MessageFacts` ─ body/rawBody/bodyForAgent/commandBody/inboundHistory
- `SupplementalContextFacts` ─ quote/forwarded/thread/untrustedContext/groupSystemPrompt

### 3.4 调用链
见 §1.4 中 step 3-10。

### 3.5 可借鉴点

1. **Facts pattern**：与其在 `processMessage(ctx)` 函数签名里塞 20 个参数，把每类信息装进自己的 Facts 类型，preflight 阶段一次性算出。后续 stages 都基于这套 Facts 派生，不再回头查 SDK。我们后端"消息编排"如果做，强烈推荐这套。
2. **`untrustedContext: Array<{ label, source, type, payload }>`**（types.ts:213）：明确标"这段内容来自用户上传/转发，不可信"，模型 prompt 里隔离展示。Prompt injection 防御的基础设施。
3. **`bot-loop-protection`**：同 bot pair 在 N 秒内交互 >M 次就抑制。AI 双向消息时常踩坑。
4. **`canStartAgentTurn` 显式标志**：reaction / lifecycle 事件分类成"不能启动 agent turn"，避免点个 emoji 反应就调 LLM。
5. **stage log events**：每 stage 五种事件 + 结构化字段，timeline 直接成排查工具。

---

## 4. `src/channels/message/`：消息生命周期

### 4.1 职责
渠道无关的"一条消息从渲染到落地、从草稿到最终化、从失败到重连"全套接口。重点是**支持 IM 原生编辑**：草稿先发空消息占位，agent 流式产生内容时持续 edit，最终 finalize 成正式消息——节省"刷屏"。

### 4.2 关键文件

| Path | 作用 |
|---|---|
| `src/channels/message/types.ts:1-368` | `MessageReceipt` / `RenderedMessageBatch` / `LiveMessageState` / `ChannelMessageSendAdapter` / `DurableFinalDeliveryRequirementMap` |
| `src/channels/message/receive.ts:1-86` | ack 状态机 |
| `src/channels/message/send.ts` (349 行) | 发送 retry + receipt 装配 |
| `src/channels/message/live.ts` | draft preview / finalize state machine |
| `src/channels/message/outbound-bridge.ts` | ReplyPayload → ChannelOutboundAdapter 桥 |
| `src/channels/message/reply-pipeline.ts` | chunk + sanitize buffered block dispatcher |
| `src/channels/message/rendered-batch.ts` | 多 part 渲染计划（text/media/voice/presentation/interactive/channelData） |
| `src/channels/message/capabilities.ts` | 渠道能力位 |
| `src/channels/message/contracts.ts` + tests | 契约测试 |

### 4.3 数据结构

**LiveMessagePhase**：`idle → previewing → finalizing → finalized | cancelled`
**LivePreviewFinalizerCapability**：`finalEdit / normalFallback / discardPending / previewReceipt / retainOnAmbiguousFailure`
**DurableFinalDeliveryCapability**（types.ts:9-22）：
```
text / media / payload / silent / replyTo / thread / nativeQuote /
messageSendingHooks / batch / reconcileUnknownSend /
afterSendSuccess / afterCommit
```
渠道声明自己能保证哪些 capability 的可恢复投递，core 按声明决定能不能走 durable 路径。

### 4.4 调用链
```
agent 产出 ReplyPayload
  → outbound-bridge 调 ChannelOutboundAdapter
  → live=true 时先 sendText 空消息拿 receipt → previewing
  → agent 继续吐 → previewUpdate → ChannelOutboundAdapter.edit
  → 完成 → finalize → 最后一次 edit 写入完整内容
durable=true 时：
  → outbound queue 写入 intent
  → 消费者按 capability 选 send path
  → 网络异常 → reconcileUnknownSend 决定 retry/commit/abandon
```

### 4.5 可借鉴点
1. **"草稿预览 + 原地最终化"**比"先发一条 'thinking...' 再追加"对用户体验好得多。代价是渠道得支持 edit。
2. **capability 矩阵**：渠道用一个 `Partial<Record<Capability, boolean>>` 声明能力，core 按能力选路径。不要硬编码"discord 支持 X 而 telegram 不支持"。
3. **`reconcileUnknownSend`**：retry 重发不一定安全（用户会收到两条），先去查渠道历史确认有没有发出去再决定。这是出站可靠投递的金标准。

---

## 5. `src/auto-reply/`：入站消息处理 & agent 回复编排

### 5.1 职责（**名字误导，是核心 inbound 处理层**）
不是"被动自动回复小工具"。包含：
- **命令检测**（`command-detection.ts`、`commands-registry*.ts`）：识别 `/start`、`/model`、`/reset` 这类原生命令
- **DM 节流**（`inbound-debounce.ts`）：同 sender 短时间多条合并
- **心跳**（`heartbeat.ts`、`heartbeat-filter.ts`）：定时唤醒 agent 看 HEARTBEAT.md
- **消息分块**（`chunk.ts`）：超长回复按字符/token 分段
- **回复 dispatch 主循环**（`dispatch.ts`、`dispatch-dispatcher.ts`、`reply.ts`、`reply.runtime.ts`、`reply/*`）
- **模型选择**（`model.ts`）
- **思考显示**（`thinking.ts`、`thinking.shared.ts`）
- **fallback 状态**（`fallback-state.ts`）：模型失败时的兜底
- **媒体注解**（`media-note.ts`）
- **token 管理**（`tokens.ts`）
- **skill commands**（`skill-commands*.ts`）
- **打字状态**（typing 在 channels/ 下，由 auto-reply 触发）

### 5.2 关键文件

| Path | 行 | 作用 |
|---|---|---|
| `src/auto-reply/dispatch.ts` | 356 | 主调度：foregroundReplyFence 防并发 + withReplyDispatcher + dispatchReplyFromConfig |
| `src/auto-reply/reply.ts` | 12 | 入口 re-export |
| `src/auto-reply/reply.runtime.ts` | - | runtime adapter（注入到 channel turn） |
| `src/auto-reply/reply/` | 子目录 | get-reply / reply-dispatcher / dispatch-from-config / history / provider-dispatcher |
| `src/auto-reply/inbound-debounce.ts` | 266 | 同 sender 短时合并（IM 用户连发 3 条触发 1 次 agent turn） |
| `src/auto-reply/heartbeat.ts` | 321 | 心跳：定时给 agent 发 "Read HEARTBEAT.md, reply HEARTBEAT_OK if nothing" |
| `src/auto-reply/heartbeat-tool-response.ts` | - | `heartbeat_respond` 工具：让 agent 决定是否通知用户 |
| `src/auto-reply/chunk.ts` | - | text chunking |
| `src/auto-reply/command-detection.ts` | 96 | 识别 native command（/前缀） |
| `src/auto-reply/commands-registry.ts` 等 | - | 命令注册表（数据 + runtime + shared） |
| `src/auto-reply/templating.ts` | - | `MsgContext` / `FinalizedMsgContext` 类型与渲染 |
| `src/auto-reply/reply-payload.ts` | - | 统一 ReplyPayload 类型（text/media/payload/interactive/presentation） |
| `src/auto-reply/envelope.ts` | - | dispatch envelope（agent 维度封装） |
| `src/auto-reply/send-policy.ts` | - | silent / 批次策略 |
| `src/auto-reply/status.ts` + `status.runtime.ts` | - | 渠道状态 reaction（✅/⏳/❌ 表情反应） |
| `src/auto-reply/group-activation.ts` | - | 群消息是否需要 @机器人才响应 |

### 5.3 数据结构

**ReplyPayload**（auto-reply/reply-payload.ts）：跨渠道统一的"agent 输出"
```ts
type ReplyPayload = {
  text?: string;
  media?: { url, mime, ... };
  interactive?: InteractiveReply;        // buttons/select/modal
  presentation?: MessagePresentation;    // 多块复合卡片
  channelData?: unknown;                 // 渠道原生 escape hatch
  silent?: boolean;
  reply?: { toId?: string };
  // ...
};
```

**MsgContext**（auto-reply/templating.ts）：agent 看到的"完整入站语境"
- 包含 Sender/Channel/From/To/ChatType/ReplyToId/ThreadLabel/Body/AgentId 等
- `FinalizedMsgContext` 是 dispatch 前最后一次填充版本

**HeartbeatTask**：
```ts
type HeartbeatTask = { name: string; interval: string; prompt: string };
```

### 5.4 调用链（dispatch 主路径）

```
ChannelTurnKernel.assemble()
  → dispatchReplyFromConfig(cfg, finalized, opts)
       → beginForegroundReplyFence (per sessionKey 串行)
       → withReplyDispatcher
            → createReplyDispatcherWithTyping
                 → channel.typing.start (channels/typing.ts)
                 → dispatchReplyWithBufferedBlockDispatcher
                      → providerRuntime.streamChatCompletion (LLM)
                      → 每个 block:
                          → block-streaming chunk decision
                          → ChannelTurnDeliveryAdapter.deliver
                               → ChannelOutboundAdapter.sendText/Media/Payload
                 → channel.typing.stop
       → endForegroundReplyFence
```

### 5.5 可借鉴点

1. **foregroundReplyFence**（dispatch.ts:39-80）：同 `(channel, accountId, sessionKey, chatType, target)` 维度的回复串行化。AI 流式期间用户又发一条，先把旧的吃完。我们做 IM bot 必踩这个坑。
2. **inbound-debounce**：用户敲一段话分 3 条发完，合并成一次 agent turn。否则 agent 会回 3 次。我们做客户群 bot 必须有。
3. **heartbeat as inbound 事件**：agent 不是只对用户消息响应，定时器、reaction、lifecycle 都被建模成"伪入站消息"走同一条 pipeline。统一抽象的威力。
4. **`silent` flag in ReplyPayload**：agent 内部回复（如更新自己的 HEARTBEAT.md）不发到 IM。同一管线两种出口。
5. **status reactions**（status.ts、status-reactions.ts）：用 IM 原生 emoji 反应表示"思考中 / 完成 / 失败"，比"再发一条文本说我在想"省噪音。

---

## 6. `src/pairing/`：DM 准入与配对

### 6.1 职责
- 任何 channel 上"陌生人"第一次 DM 时，**不放进 agent**，先发"配对码 + ID 行"挑战
- 用户在 setup CLI/TUI 把码贴回去，写入 `allow-from-*.json`（per-channel + per-account）
- 下次再 DM，allowlist 命中 → 直通

### 6.2 关键文件

| Path | 行 | 作用 |
|---|---|---|
| `src/pairing/pairing-store.ts` | 691 | pending pairing 请求存储（TTL 60min、max 3 pending、原子写 + 文件锁） |
| `src/pairing/pairing-store.types.ts` | 19 | `PairingChannel` 类型 |
| `src/pairing/pairing-challenge.ts` | 48 | `issuePairingChallenge`：upsert pending + send reply 一体 |
| `src/pairing/pairing-messages.ts` | 26 | `buildPairingReply({ channel, idLine, code })` |
| `src/pairing/pairing-labels.ts` | 6 | label 常量 |
| `src/pairing/allow-from-store-file.ts` | 348 | allow-from JSON 文件读写、缓存、legacy 兼容 |
| `src/pairing/allow-from-store-read.ts` | 63 | 只读 path |
| `src/pairing/setup-code.ts` | 408 | **设备配对**（不同于 DM 配对！）：生成 `device-pair` 引导 URL + bootstrap token，用于把手机/桌面 client 接到 gateway。涉及 Tailscale / public URL / wss 校验 |

### 6.3 数据结构
```ts
type PairingRequest = {
  id: string;              // sender id（如 discord user id）
  code: string;            // 8 位大写字母数字（避免易混字符 ILO0/1）
  createdAt: string;
  lastSeenAt: string;
  meta?: Record<string, string>;
};

type PairingStore = { version: 1; requests: PairingRequest[] };
```
存储路径：`<credentialsDir>/<channel>-pairing.json`
allowlist：`<credentialsDir>/<channel>-<accountId>-allow-from.json`

### 6.4 调用链
```
inbound DM → channels/plugins/pairing.ts: ChannelPairingAdapter.shouldChallenge?
  ✓ 进入 pairing 路径：
    issuePairingChallenge({
      channel, senderId, senderIdLine,
      upsertPairingRequest: pairingStore.upsert,    // 60min TTL + max 3
      sendPairingReply: outbound.sendText,
    })
    → admission = "handled"，turn 结束（不 dispatch agent）

setup CLI/TUI：
  user 输入 code → pairing-store.approveByCode → 写 allow-from JSON → 清掉 pending

下一条 inbound DM：
  allow-from 命中 → admission = "dispatch"
```

### 6.5 可借鉴点

1. **per-channel + per-account JSON 文件存储 + atomic write + file lock**：避免 setup TUI 和 inbound runtime 抢写。我们做"哪些 IM 用户能调 AI"也是同一架构。
2. **配对码字符集 `ABCDEFGHJKLMNPQRSTUVWXYZ23456789`**：排除 I / L / O / 0 / 1。复刻。
3. **DM 配对 vs 设备配对分两套**：`pairing-store.ts` 管 IM 用户的 DM 准入，`setup-code.ts` 管"把手机 client 接到 gateway"。命名空间清晰，不要混在一个 store。
4. **PAIRING_PENDING_MAX = 3 + TTL = 60min**：防 DM 配对码刷码攻击。
5. **`pairing-adapters.ts` 在 plugins/ 下**，让每个渠道自己声明"DM 进来怎么挑战 / 怎么发码 / 怎么解析回执"。core 只编排，不写渠道细节。

---

## 7. `src/auto-reply/` 中的命令、心跳与 polls 全景

### 7.1 polls.ts / poll-params.ts

`/tmp/openclaw-fresh/src/polls.ts`（100 行）+ `poll-params.ts`（124 行）：

**polls = IM 群投票（vote），不是消息轮询（polling）**。

`PollInput { question, options[], maxSelections?, durationSeconds?|durationHours? }` 经 `normalizePollInput` 校验后下发给各渠道：
- discord：sendPoll
- telegram：sendPoll
- whatsapp：sendPoll
- slack：block kit poll
- 其余降级到 sendText(question + 选项)

调用入口：agent 通过 `message(action=poll)` 工具，由 `ChannelMessageActionAdapter.handleAction` 落地。

### 7.2 心跳 heartbeat

`auto-reply/heartbeat.ts`：定时（默认 30min）注入伪入站消息触发 agent turn，prompt = "Read HEARTBEAT.md，无事回 HEARTBEAT_OK"。agent 通过 `heartbeat_respond(notify?, notificationText?)` 工具决定要不要打扰用户。`heartbeat-filter.ts` 过滤"effectively empty" HEARTBEAT.md 内容来节省 LLM 调用。

### 7.3 命令系统

`auto-reply/command-detection.ts` + `commands-registry.*.ts`：
- 数据层（`.data.ts`）：命令定义
- 注册层（`.runtime.ts`、`.shared.ts`）：注册到 runtime
- 文本路由（`commands-text-routing.ts`）：识别 "/x args" 文本
- args 解析（`commands-args.ts`）：类型化参数

命令在 turn 状态机的 preflight 阶段被识别为 admission=handled，**不走 agent**，直接调对应 handler。典型命令：`/help`、`/model gpt-4o`、`/reset`、`/skills`。

---

## 8. `extensions/` 中的渠道扩展实战

### 8.1 注册模式（所有 channel ext 通用）

```ts
// extensions/discord/index.ts
import { defineBundledChannelEntry } from "openclaw/plugin-sdk/channel-entry-contract";

export default defineBundledChannelEntry({
  id: "discord",
  name: "Discord",
  importMetaUrl: import.meta.url,
  plugin:           { specifier: "./channel-plugin-api.js",  exportName: "discordPlugin" },
  runtime:          { specifier: "./runtime-setter-api.js",  exportName: "setDiscordRuntime" },
  accountInspect:   { specifier: "./account-inspect-api.js", exportName: "inspectDiscordReadOnlyAccount" },
  registerFull(api) { registerDiscordSubagentHooks(api); },
});
```

```json
// extensions/discord/openclaw.plugin.json
{
  "id": "discord",
  "activation": { "onStartup": false },
  "channels": ["discord"],
  "channelEnvVars": { "discord": ["DISCORD_BOT_TOKEN"] },
  "configSchema": { "type": "object", "additionalProperties": false, "properties": {} }
}
```

```ts
// extensions/discord/src/channel.ts（节选）
const discordMessageAdapter = createChannelMessageAdapterFromOutbound({
  id: "discord",
  outbound: discordOutbound,
  live: {
    capabilities: { draftPreview: true, previewFinalization: true, progressUpdates: true },
    finalizer: { capabilities: { finalEdit: true, normalFallback: true, discardPending: true } },
  },
});

export const discordPlugin = createChatChannelPlugin({
  id: "discord",
  meta: ...,
  capabilities: ...,
  messaging:     { ... parseDiscordExplicitTarget, resolveSessionConversation, ... },
  messageAdapter: discordMessageAdapter,
  outbound:       discordOutbound,
  messageActions: discordMessageActions,
  security:       discordSecurityAdapter,
  ...
});
```

### 8.2 v2026.5.14 实际包含的 24 个渠道相关 extension

按 `ls extensions/` 实测（过滤 IM/渠道类，排除 LLM provider / browser / audio 等）：

| Extension | 类型 | 备注 |
|---|---|---|
| `device-pair` | 配对辅助 | 不是聊天渠道，是 mobile client 接 gateway 的引导工具 |
| `discord` | bot | gateway WS + REST，最完整实现，约 60+ src 文件 |
| `feishu` / 飞书 | bot | 国内企业 IM |
| `googlechat` | bot | Google Workspace |
| `imessage` | macOS native | 同时支持本地 imessage CLI 和 BlueBubbles HTTP 后端 |
| `irc` | classic IRC | |
| `line` | LINE bot | 日本 |
| `matrix` | federated | element / synapse |
| `mattermost` | self-hosted Slack-alike | |
| `msteams` | Microsoft Teams | |
| `nextcloud-talk` | Nextcloud 自托管 | |
| `nostr` | 分布式协议 | 去中心化、relay 模型 |
| `qa-channel` | 测试渠道 | 内部 QA harness |
| `qa-matrix` | 测试矩阵 | 用 matrix 跑契约测试的 QA 工具 |
| `signal` | Signal Messenger | |
| `slack` | Slack | |
| `synology-chat` | Synology Chat | |
| `telegram` | Telegram bot | Bot API |
| `tlon` | Urbit Tlon | |
| `twitch` | Twitch chat | |
| `webhooks` | 通用 webhook | 提供 inbound HTTP endpoint，给"自家系统接出去"用 |
| `whatsapp` | WhatsApp Web | 内部走 baileys（QR 配对、二维码扫描在这里，**不在** src/web/） |
| `zalo` | Zalo bot | 越南 |
| `zalouser` | Zalo user mode | 个人号而非 bot |

确认两个易混点：
- **"WhatsApp Web QR 配对"在 `extensions/whatsapp/`**，不在 `src/web/`。`src/web/` 在 v2026.5.14 只剩 `provider-runtime-shared.ts`（LLM provider runtime 工具），与渠道无关。
- **bluebubbles 不是顶层 extension**，已合并到 `extensions/imessage/`（作为 BlueBubbles 后端模式）。
- **m365-tools 不是顶层 extension**，相关工具集进了 `extensions/msteams/` 或 microsoft auth 扩展。

### 8.3 各渠道扩展典型文件清单（以 discord 为模板）

```
openclaw.plugin.json                  manifest（id/activation/channels/envVars/configSchema）
package.json                          npm 依赖
index.ts                              defineBundledChannelEntry 入口
channel-plugin-api.ts                 plugin 导出薄壳（避免重 import）
runtime-api.ts / runtime-setter-api.ts  runtime 注入与读取
runtime-api.send.ts / .lookup.ts / .monitor.ts / .threads.ts / .actions.ts  按职责分模块
contract-api.ts                       契约测试入口
account-inspect-api.ts                read-only 账户检查
config-api.ts / channel-config-api.ts 配置 schema
secret-contract-api.ts                凭据契约
security-contract-api.ts              安全/审计契约
setup-entry.ts                        setup wizard 入口
session-key-api.ts                    会话 key 解析（thread/topic 维度）
subagent-hooks-api.ts                 sub-agent hooks
src/channel.ts                        plugin 主体（createChatChannelPlugin 调用）
src/accounts.ts                       多账号支持
src/audit.ts / audit-core.ts          审计日志
src/approval-*.ts                     工具调用授权（IM 内点确认）
src/channel-actions.ts                messageActions adapter
src/channel.conversation.ts           inbound conversation 解析
src/channel.loaders.ts                按需 loader（避免冷启动加载所有依赖）
src/channel.message-adapter.test.ts   message adapter 测试
src/channel.runtime.ts                runtime state
src/channel.setup.ts                  setup
src/chunk.ts                          channel-specific chunking
src/client.ts                         IM SDK 客户端
src/components.*                      渠道原生组件（buttons/modal/etc）
src/components-registry.ts            组件注册表
```

### 8.4 可借鉴点

1. **plugin entry / config / runtime / setup 各自独立薄壳文件**：避免 cold path 拉重依赖。我们后端模块按"门面文件 + 实现文件"拆同理。
2. **manifest 里 `channelEnvVars` 声明环境变量**：让 startup 阶段就能告诉用户"DISCORD_BOT_TOKEN 没设"，不要 runtime 第一条消息才报错。我们后端 .env.example 检查可以借鉴。
3. **多账号原生支持**（`accounts.ts`、`accountId` 贯穿全部 adapter）：一个 channel plugin 同时管 N 个 bot token。FFOA 后端如果接客户群 bot，必须从 day 1 这么设计。

---

## 9. 总结：给 FFOA 自研 Agent 内核的 10 条关键借鉴

1. **Channel 拆 30 个小 adapter，不要做大 interface**。每个渠道按需填，core 不写 channel switch。
2. **Turn 状态机 9 stages + admission 4 态**。`handled` / `observeOnly` / `drop` 三个中间态必须分开。
3. **Facts pattern** 替代"大 ctx 对象 + 20 参数"。preflight 一次性算出，后续 stage 不回头查 SDK。
4. **ack 四档**（after_receive_record / after_agent_dispatch / after_durable_send / manual）。webhook 处理必须分档。
5. **MessageReceipt 多 part**。一次回复可能是 N 条 IM 消息，receipt 要能锚定每条，后续 edit/delete 才可行。
6. **草稿预览 + 原地 finalize**比"先发占位 + 追加"用户体验好。代价是渠道得支持 edit。
7. **`reconcileUnknownSend`**：网络抖动时先查回执再决定 retry/commit。出站可靠投递金标准。
8. **配对码挑战 + per-channel allow-from JSON**：陌生人 DM 先发码不放进 agent。`pairing-store.ts` 是参考实现，含 TTL + max pending + 原子写 + 文件锁。
9. **foregroundReplyFence**：同 session 串行化 agent 回复。流式期间用户又发一条，先吃完旧的。
10. **plugin 自描述 message tool schema**（`describeMessageTool`）：让渠道告诉 agent 自己有什么 action / capability，不要在 core 硬编码"discord 能 react、telegram 不能 react"。

---

## 10. 附录：源码定位速查

| 你想看 | 去哪 |
|---|---|
| Channel adapter 接口定义 | `src/channels/plugins/types.core.ts` |
| Turn 状态机契约 | `src/channels/turn/types.ts` |
| Turn 状态机执行体 | `src/channels/turn/kernel.ts` |
| 消息 ack 状态机 | `src/channels/message/receive.ts` |
| 出站 receipt / batch | `src/channels/message/types.ts` |
| 通用出站工厂 | `src/channels/plugins/outbound/direct-text-media.ts` |
| 出站懒加载 | `src/channels/plugins/outbound/load.ts` |
| plugin 注册 | `src/channels/plugins/registry.ts` + `bundled.ts` + `registry-loader.ts` |
| inbound dispatch 主循环 | `src/auto-reply/dispatch.ts` |
| 心跳 | `src/auto-reply/heartbeat.ts` |
| inbound 节流 | `src/auto-reply/inbound-debounce.ts` |
| 命令检测 | `src/auto-reply/command-detection.ts` + `commands-registry.*.ts` |
| poll 输入归一化 | `src/polls.ts` |
| DM 配对码 + allow-from | `src/pairing/pairing-store.ts` + `allow-from-store-file.ts` |
| 设备配对（mobile → gateway） | `src/pairing/setup-code.ts` |
| 渠道 ext 入口模板 | `extensions/discord/index.ts` + `src/channel.ts` |
| 渠道 manifest 模板 | `extensions/discord/openclaw.plugin.json` |


---

# 三、扩展性层（OpenClaw v2026.5.14）

> 范围：plugin / extension / skill / hook / command / cli / wizard 七个子系统。
> 仓库：`/tmp/openclaw-fresh/`，版本 `2026.5.14`（来自 `extensions/anthropic/package.json:3` 等多处）。
> 核心原则：**core 只搭舞台，能力以 plugin/extension/skill 形式交付**——`extensions/` 已涨到 **133 个**、`skills/` **53 个**，`packages/` 新增 4 个对外契约包（`plugin-sdk` / `plugin-package-contract` / `sdk` / `memory-host-sdk`）。

---

## 3.0 总览：扩展点三层金字塔

OpenClaw 的扩展性按"接近内核程度"分三层，每层有独立的契约面和加载机制：

| 层 | 形态 | 加载时机 | 信任级 | 数量 | 典型代表 |
|---|---|---|---|---|---|
| L1 内核扩展 | **extension**（bundled plugin） | 启动 / 按需 | trustedLocalCode | 133 | anthropic、codex、discord、memory-core |
| L2 用户扩展 | **plugin**（外部代码包，可上 ClawHub） | 按需安装 | 受 plugin-package-contract 校验 | n/a | 任意 `@openclaw/*-plugin` |
| L3 行为脚本 | **skill** / **hook** / **command** | 运行期动态 | workspace 默认 opt-in | 53 skill + 5 内置 hook + 130+ 子命令 | coding-agent、discord、weather |

**三者边界**：
- **Extension/Plugin** = 注册"能力"（provider、tool、channel、memory backend）——动 Node 代码，进 plugin runtime API。
- **Skill** = 注册"工作流文档"——只是 Markdown + frontmatter，被 agent prompt 拼接、不动 Node 代码。
- **Hook** = 注册"事件回调"——监听 command/session/agent/gateway/message 五类事件，可改输出或注入消息。

下面分子系统展开。

---

## 3.1 Plugin SDK 边界契约（packages/plugin-sdk）

### 职责
为 plugin 提供**稳定、版本化、subpath-import 的公开 API 契约面**。这是 OpenClaw 内部 `src/plugin-sdk/` 实现的**对外门面**——extensions 和第三方 plugins 只允许 import `openclaw/plugin-sdk/<subpath>`，禁止 reach into `src/`（详见 `loader-sdk-import-guardrails.test.ts`）。

### 关键文件
- `packages/plugin-sdk/package.json:1-220`：声明 **63 个 subpath exports**（每个对应一份 .d.ts + 一份 .ts），构成完整的契约面清单。
- `packages/plugin-sdk/src/plugin-entry.ts:1`：thin re-export 自 `src/plugin-sdk/plugin-entry.ts`。
- `src/plugin-sdk/plugin-entry.ts:260-322`：`definePluginEntry()` 工厂——所有 extension 的 default export 都走这个。
- `src/plugins/types.ts:2570-2832`：`OpenClawPluginApi` 完整声明，所有 `register*` 方法在此（共 12+ 注册点）。
- `src/plugin-activation-boundary.test.ts:1-200+`：用例锁定"plugin 激活前不得访问内核状态"的边界——core 启动期间禁止 plugin 写。

### 数据结构

`@openclaw/plugin-sdk` 公开的 63 个 subpath 按职能分类：

| 类别 | subpath | 用途 |
|---|---|---|
| 入口 | `plugin-entry` / `provider-entry` / `plugin-runtime` | extension default export 用 |
| Provider 契约 | `provider-auth` / `provider-auth-runtime` / `provider-http` / `provider-stream-shared` / `provider-tools` / `provider-onboard` / `provider-model-types` / `provider-model-shared` / `provider-env-vars` / `provider-usage` / `provider-web-search` / `provider-web-search-contract` | LLM provider 写法 |
| Channel 契约 | `channel-runtime` / `channel-streaming` / `channel-secret-runtime` / `channel-activity-runtime` | IM 渠道插件 |
| Runtime 注入 | `time-runtime` / `number-runtime` / `secure-random-runtime` / `concurrency-runtime` / `dedupe-runtime` / `async-lock-runtime` / `system-event-runtime` / `transport-ready-runtime` / `heartbeat-runtime` / `delivery-queue-runtime` / `file-access-runtime` / `cron-store-runtime` / `model-session-runtime` | 解耦"plugin 不直连 Node API/全局状态" |
| Config 契约 | `config-runtime` / `config-types` / `config-contracts` / `config-mutation` / `plugin-config-runtime` | plugin 读写自己的 config 切片 |
| Security | `security-runtime` / `secret-ref-runtime` / `ssrf-runtime` / `secret-input` | secret 管理 + SSRF 防护 |
| Tooling | `cli-runtime` / `error-runtime` / `runtime-env` / `runtime-doctor` / `infra-runtime` / `tts-runtime` / `text-runtime` / `text-utility-runtime` / `talk-config-runtime` / `video-generation` / `testing` / `compat` / `core` / `zod` | 其它工具面 |

### 调用链

plugin 加载链路：

```
extensions/<name>/index.ts
  └─ default export = definePluginEntry({ id, register })
       └─ register(api: OpenClawPluginApi)
            ├─ api.registerProvider(...)         // LLM provider
            ├─ api.registerWebSearchProvider(...) // web search
            ├─ api.registerChannel(...)           // IM channel
            ├─ api.registerTool(...)              // agent tool
            ├─ api.registerCommand(...)           // /command
            ├─ api.registerMemoryCapability(...)  // memory backend
            ├─ api.registerToolMetadata(...)
            ├─ api.registerCli(...)               // subcommand
            └─ ... 共 12+ 注册点
       │
       └─ src/plugins/loader.ts: discoverBundledPlugins()
            └─ 读取 extensions/*/openclaw.plugin.json 清单
            └─ 校验 src/plugins/manifest-contract-runtime.ts
            └─ 创建 plugin sandbox（src/plugins/api-builder.ts）
            └─ 调用 entry.register(api)
            └─ 把注册结果合入全局 capability registry
```

### 可借鉴点
1. **subpath exports 而非单一 index**：63 个 subpath 让 plugin 只暴露真正用到的 API，treeshake 友好，**契约面变更可按 subpath 粒度做废弃公告**。
2. **runtime 注入 vs Node 全局**：`time-runtime` / `secure-random-runtime` 让 plugin **不要直接 `Date.now()` / `crypto.randomUUID()`**——测试时可注入 deterministic 版本，生产时可统一审计。这是把"side-effect 显式化"做到极致。
3. **`definePluginEntry` 工厂返回的 schema 是 lazy**（`createCachedLazyValueGetter`）——避免冷启动跑遍所有 plugin 的 zod schema。

---

## 3.2 packages/plugin-package-contract（npm 包契约）

### 职责
**校验外部 plugin npm 包能不能被 ClawHub 接收 / 被 OpenClaw 主机加载**。是 npm package.json 里 `openclaw` 块的契约定义和 normalizer。

### 关键文件
- `packages/plugin-package-contract/src/index.ts:1-103`：完整实现。
- `packages/plugin-package-contract/src/index.test.ts`：用例。

### 数据结构
plugin 必须在 `package.json` 里声明：

```json
{
  "openclaw": {
    "compat": {
      "pluginApi": "^2026.5",          // 必填，SDK API range
      "minGatewayVersion": "2026.5.0"  // 可选
    },
    "build": {
      "openclawVersion": "2026.5.14",  // 必填，构建时的 host 版本
      "pluginSdkVersion": "..."        // 可选
    },
    "install": {
      "minHostVersion": "..."          // 老字段，fallback 给 minGatewayVersion
    }
  }
}
```

API：
- `normalizeExternalPluginCompatibility(pkg)` → `{ pluginApiRange, builtWithOpenClawVersion, pluginSdkVersion, minGatewayVersion }`
- `listMissingExternalCodePluginFieldPaths(pkg)` → 缺失字段路径数组
- `validateExternalCodePluginPackageJson(pkg)` → `{ compatibility, issues[] }`，issues 是 `{ fieldPath, message }`

必填路径（`EXTERNAL_CODE_PLUGIN_REQUIRED_FIELD_PATHS`）：
- `openclaw.compat.pluginApi`
- `openclaw.build.openclawVersion`

### 调用链
- **ClawHub 上架**：publish 时跑 `validateExternalCodePluginPackageJson()`，issues 非空直接拒。
- **本地安装**：`src/plugins/clawhub.ts` 下载 tarball 后用 `satisfiesPluginApiRange()` / `satisfiesGatewayMinimum()` 对比当前 host，不匹配抛 `INCOMPATIBLE_PLUGIN_API` / `INCOMPATIBLE_GATEWAY`（见 `src/plugins/clawhub.ts:40-54` 的 `CLAWHUB_INSTALL_ERROR_CODE`）。

### 可借鉴点
1. **把 npm 包契约写成独立 package**——publisher、registry、host loader 三方共用同一份 normalize/validate 函数，不会出现"registry 接受了但 host 装不上"的不一致。
2. **`pluginApi` 用 semver range**（不是固定版本）——SDK 改 minor 时旧 plugin 仍可用，明确的兼容性语言。
3. **issues 是结构化 `{fieldPath, message}` 而非 string**——便于 UI 高亮和自动修复。

---

## 3.3 packages/sdk vs packages/plugin-sdk

两者**完全不同**，**不要混淆**。

| 维度 | `@openclaw/plugin-sdk` | `@openclaw/sdk` |
|---|---|---|
| 受众 | plugin 作者（写 extension 的人） | OpenClaw API 调用方（外部应用 / 集成方） |
| 形态 | 63 个 subpath，无 dist build | 单一入口 `dist/index.mjs`，tsdown 打包 |
| 关键导出 | `definePluginEntry` / `OpenClawPluginApi` / 各类 runtime 注入 | `OpenClaw` 客户端类、`AgentsNamespace` / `SessionsNamespace` / `TasksNamespace` / `RunsNamespace` / `EventHub` / `GatewayClientTransport` |
| 通信对端 | host 进程内 plugin runtime | 跨进程 / 跨网络 gateway |
| 类比 | "VSCode 扩展 API" | "VSCode CLI/HTTP API SDK" |

### 关键文件
- `packages/sdk/package.json`：单一 `./` export，build 用 `tsdown src/index.ts --no-config --platform node --format esm --dts`。
- `packages/sdk/src/index.ts:1-55`：导出 `OpenClaw` 客户端 + 8 个 namespace + `GatewayEvent` 类型。
- `packages/sdk/src/client.ts` / `event-hub.ts` / `transport.ts`：实现。

### 调用链（@openclaw/sdk）
```
外部 Node app
  └─ import { OpenClaw } from "@openclaw/sdk"
       └─ new OpenClaw({ transport: GatewayClientTransport(...) })
            ├─ .agents.list() / .sessions.send() / .runs.create() ...
            └─ .events.subscribe()  // SSE/WS 流
```

### 可借鉴点
1. **sdk vs plugin-sdk 是两层契约**：plugin-sdk 是"进程内 plugin 写法"，sdk 是"跨进程客户端 SDK"——很多开源项目把这两层混在一个包里，扩展性和可发布性都会受限。
2. **plugin-sdk 不打 dist**（`private: true` + 直接 `.ts` exports），sdk 打 dist——因为 plugin 走 workspace 链接、sdk 是要发 npm 的。

---

## 3.4 packages/memory-host-sdk（v2026.5.14 新增）

### 职责
专门给 **memory plugin**（如 `memory-core`、`memory-lancedb`、`memory-wiki`、`active-memory`）使用的 host runtime SDK——把 memory 子系统的存储引擎、QMD 查询、embedding provider、secret、status 等设施抽出来。

### 关键文件
- `packages/memory-host-sdk/package.json`：14 个 subpath，包括 `runtime` / `runtime-core` / `runtime-cli` / `runtime-files` / `engine` / `engine-foundation` / `engine-storage` / `engine-embeddings` / `engine-qmd` / `multimodal` / `query` / `secret` / `status`。
- `packages/memory-host-sdk/src/engine-*.ts`：存储/embeddings/QMD 引擎抽象。

### 数据结构
按"引擎职责切片"组织：
- `engine-foundation`：内存模型基类
- `engine-storage`：持久化（sqlite / file / lancedb）
- `engine-embeddings`：向量化 provider 适配
- `engine-qmd`：QMD（Query Memory Documents）DSL
- `multimodal`：图/音/视频嵌入
- `runtime-*`：进程内 / CLI / 文件三种宿主形态

### 调用链
```
extensions/memory-core/index.ts
  └─ import { resolveMemorySearchConfig, MemoryPluginRuntime } from
       "openclaw/plugin-sdk/memory-core-host-runtime-core"
  └─ import { resolveMemoryBackendConfig } from
       "openclaw/plugin-sdk/memory-core-host-runtime-files"
  └─ api.registerMemoryCapability({ runtime, promptBuilder, flushPlanResolver })
```

### 可借鉴点
1. **垂直子系统单独抽 SDK 包**——memory 是 OpenClaw 里"plugin 实现最多、内核交互最深"的子系统，单独抽 host-sdk 让 4-5 个 memory 插件共享同一份引擎契约。
2. **engine vs runtime 切分**：engine 是无状态算法（embeddings、QMD parser），runtime 是有状态的宿主（CLI、files、core）——便于在 server / CLI / IDE 三种场景复用。

---

## 3.5 Extensions（133 个 bundled plugin）

### 职责
OpenClaw **随主仓库一起发布的内置 plugin**——每个 extension 是一个独立 npm 包（workspace package），有 `package.json` + `openclaw.plugin.json` + `index.ts`。这是 "core 精简、能力 plugin 化"的具体落地。

### 关键文件
- `extensions/<name>/package.json`：声明 `openclaw.extensions: ["./index.ts"]`。
- `extensions/<name>/openclaw.plugin.json`：manifest（`id` / `activation` / `providers` / `contracts` / `configSchema` / `uiHints` 等）。
- `extensions/<name>/index.ts`：default export `definePluginEntry(...)` 或 `defineBundledChannelEntry(...)`。
- `src/plugins/loader.ts` + `src/plugins/bundled-plugin-scan.ts`：扫描 + 加载。
- `extensions/tsconfig.package-boundary.base.json` / `tsconfig.package-boundary.paths.json`：tsc 边界约束（plugin 不许 reach into `src/`）。

### v2026.5.14 全部 133 个 extensions（按职能分类）

**LLM Provider（30+）**：
`alibaba` / `amazon-bedrock` / `amazon-bedrock-mantle` / `anthropic` / `anthropic-vertex` / `arcee` / `byteplus` / `cerebras` / `chutes` / `cloudflare-ai-gateway` / `codex` / `copilot-proxy` / `deepinfra` / `deepseek` / `fireworks` / `google` / `groq` / `huggingface` / `inworld` / `kilocode` / `kimi-coding` / `lmstudio` / `litellm` / `microsoft` / `microsoft-foundry` / `minimax` / `mistral` / `moonshot` / `nvidia` / `ollama` / `openai` / `opencode` / `opencode-go` / `openrouter` / `perplexity` / `qianfan` / `qwen` / `sglang` / `stepfun` / `synthetic` / `tencent` / `together` / `tokenjuice` / `vercel-ai-gateway` / `venice` / `vllm` / `volcengine` / `voyage` / `xai` / `zai`

**Coding/Agent CLI Backend**：
`anthropic`（claude-cli）/ `codex` / `kimi-coding` / `opencode` / `opencode-go` / `migrate-claude` / `migrate-hermes`

**IM/Channel（25+）**：
`discord` / `feishu` / `googlechat` / `imessage` / `irc` / `line` / `matrix` / `mattermost` / `msteams` / `nostr` / `qqbot` / `signal` / `slack` / `synology-chat` / `telegram` / `tlon` / `twitch` / `webhooks` / `whatsapp` / `xiaomi` / `zalo` / `zalouser` / `nextcloud-talk` / `qa-channel` / `gradium` / `vydra` / `lobster`

**搜索/抓取**：
`brave` / `duckduckgo` / `exa` / `firecrawl` / `searxng` / `tavily` / `web-readability`

**媒体（音视频/图像）**：
`azure-speech` / `comfy` / `deepgram` / `elevenlabs` / `fal` / `image-generation-core` / `media-understanding-core` / `music-generation-providers.live.test.ts` / `runway` / `senseaudio` / `speech-core` / `talk-voice` / `tts-local-cli` / `video-generation-core` / `voice-call`

**Memory**：
`active-memory` / `memory-core` / `memory-lancedb` / `memory-wiki`

**诊断/可观测**：
`diagnostics-otel` / `diagnostics-prometheus` / `test-support` / `qa-lab` / `qa-matrix`

**设备/IO/桥接**：
`acpx` / `bonjour` / `browser` / `clickclack` / `device-pair` / `diffs` / `document-extract` / `file-transfer` / `image-generation-core` / `media-understanding-core` / `oc-path` / `openshell` / `phone-control` / `skill-workshop` / `thread-ownership`

**工具**：
`canvas` / `github-copilot` / `llm-task` / `open-prose` / `qa-channel`

### Extension manifest 字段（以 `anthropic/openclaw.plugin.json` 为例）
```json
{
  "id": "anthropic",
  "activation": { "onStartup": false },
  "enabledByDefault": true,
  "providers": ["anthropic"],
  "providerCatalogEntry": "./provider-discovery.ts",
  "modelSupport": { "modelPrefixes": ["claude-"] },
  "modelIdNormalization": { ... },
  "providerEndpoints": [...],
  "cliBackends": ["claude-cli"],
  "syntheticAuthRefs": ["claude-cli"],
  "providerAuthEnvVars": { "anthropic": ["ANTHROPIC_OAUTH_TOKEN", "ANTHROPIC_API_KEY"] },
  "providerAuthChoices": [ ... ],
  "contracts": { "mediaUnderstandingProviders": ["anthropic"] },
  "configSchema": { "type": "object", ... }
}
```

### 注册方式（两种入口）

**普通 plugin**（provider / tool / web search / memory）：
```typescript
// extensions/exa/index.ts
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
import { createExaWebSearchProvider } from "./src/exa-web-search-provider.js";

export default definePluginEntry({
  id: "exa",
  name: "Exa Plugin",
  description: "Bundled Exa web search plugin",
  register(api) {
    api.registerWebSearchProvider(createExaWebSearchProvider());
  },
});
```

**Channel plugin**（IM 渠道，wiring 更重）：
```typescript
// extensions/discord/index.ts
import { defineBundledChannelEntry } from "openclaw/plugin-sdk/channel-entry-contract";
export default defineBundledChannelEntry({
  id: "discord",
  name: "Discord",
  importMetaUrl: import.meta.url,
  plugin: { specifier: "./channel-plugin-api.js", exportName: "discordPlugin" },
  runtime: { specifier: "./runtime-setter-api.js", exportName: "setDiscordRuntime" },
  accountInspect: { specifier: "./account-inspect-api.js", exportName: "inspectDiscordReadOnlyAccount" },
  ...
});
```

### 是否沙箱化
**不是 OS 级沙箱**——extension 是同进程 Node 代码，能 require/import 任意模块。但有三层软约束：
1. **import 边界**：plugin 只能 import `openclaw/plugin-sdk/*` 子路径，不能 reach `src/*`（`loader-sdk-import-guardrails.test.ts` 锁定）。
2. **API 边界**：plugin 拿到的 `api: OpenClawPluginApi` 是显式构造的 facade（`src/plugins/api-builder.ts` / `api-facades.ts`），不暴露 host 全局。
3. **激活边界**：`activation.onStartup: false` 的 plugin 只在被需要时（provider 被选中、tool 被调用）才 `register()`——大幅缩短冷启动（见 `src/plugins/activation-planner.ts`）。

### 可借鉴点
1. **manifest 与代码分离**：`openclaw.plugin.json` 让 host 在**不 import plugin 代码**的情况下就能拿到 capability metadata——配置 UI、catalog 列表、auth choice 都靠 manifest 静态生成，避免冷启动跑遍 plugin code。
2. **activation lazy 化**：默认 `onStartup: false`，能力靠"被引用"触发激活。
3. **Provider auth choices declarative**：登录方式（CLI / OAuth / API key / setup-token）写在 manifest 的 `providerAuthChoices` 数组里，wizard 直接渲染——不需要每个 provider 自己写 prompt 代码。
4. **每个 extension 是独立 npm 包**：133 个 workspace 包，可以单独被 ClawHub 抽出来发，也可以一起 bundle 进主 OpenClaw 发布物。

---

## 3.6 Skills（53 个工作流脚本）

### 职责
Skill 是**最轻量的扩展形态**——一份 Markdown 文件（`SKILL.md`），带 YAML frontmatter 声明 metadata + 依赖。agent 在 prompt 里把激活的 skill 文本拼进 system prompt，**不动 Node 代码**。

### 关键文件
- `skills/<name>/SKILL.md`：唯一必需文件，`---` 包裹的 frontmatter + Markdown body。
- `src/cli/skills-cli.ts`：`openclaw skills list/install/check/info` CLI。
- `src/agents/skills-clawhub.ts`：从 ClawHub 安装/更新 skill 的逻辑。
- `src/agents/skills-status.ts`：检查 skill `requires.bins/env/config` 是否就绪。

### v2026.5.14 全部 53 个 skills
`1password` / `apple-notes` / `apple-reminders` / `bear-notes` / `blogwatcher` / `blucli` / `camsnap` / `canvas` / `clawhub` / `coding-agent` / `discord` / `eightctl` / `gemini` / `gh-issues` / `gifgrep` / `github` / `gog` / `goplaces` / `healthcheck` / `himalaya` / `imsg` / `mcporter` / `model-usage` / `nano-pdf` / `node-connect` / `notion` / `obsidian` / `openai-whisper` / `openai-whisper-api` / `openhue` / `oracle` / `ordercli` / `peekaboo` / `pyproject.toml` / `sag` / `session-logs` / `sherpa-onnx-tts` / `skill-creator` / `slack` / `songsee` / `sonoscli` / `spotify-player` / `summarize` / `taskflow` / `taskflow-inbox-triage` / `things-mac` / `tmux` / `trello` / `video-frames` / `voice-call` / `wacli` / `weather` / `xurl`

> 注：`pyproject.toml` 出现在列表里像是路径噪音，其它 52 个是真正的 skill。

### SKILL.md 格式（以 `coding-agent` 为例）

```yaml
---
name: coding-agent
description: 'Delegate coding tasks to Codex, Claude Code, OpenCode, or Pi agents...'
metadata:
  openclaw:
    emoji: "🧩"
    requires:
      anyBins: ["claude", "codex", "opencode", "pi"]
      config: ["skills.entries.coding-agent.enabled"]
    install:
      - id: node-claude
        kind: node
        package: "@anthropic-ai/claude-code"
        bins: ["claude"]
        label: "Install Claude Code CLI (npm)"
      - id: node-codex
        kind: node
        ...
---

# Coding Agent

<Markdown body 提供工作流说明>
```

**frontmatter 字段**：
- `name` / `description`：必填，UI 展示。
- `homepage`：可选。
- `allowed-tools`：可选数组，限制本 skill 期间 agent 可用的 tool（如 discord skill 只让用 `message`）。
- `user-invocable`：可选 bool，是否允许用户在 chat 里 `@skill-name` 显式触发。
- `metadata.openclaw.emoji`：UI 图标。
- `metadata.openclaw.requires.bins/anyBins/env/config`：依赖声明，`skills check` 会一项项校验。
- `metadata.openclaw.install`：声明式安装步骤（kind: node / brew / apt / npm / git），让 `skill install` 能跨平台自动装依赖。
- `metadata.openclaw.primaryEnv`：主要环境变量名（如 `gh-issues` 的 `GH_TOKEN`）。

### 调用链
```
agent turn prepare
  └─ 扫描 workspace/agent enabled skills
  └─ buildWorkspaceSkillStatus()：核对 requires 是否就绪
  └─ 对就绪 skill：把 SKILL.md body 注入 system prompt
  └─ allowed-tools 限制本 turn 的 tool surface
```

ClawHub 安装：
```
$ openclaw skills install <slug>
  └─ installSkillFromClawHub(slug)
       └─ ClawHub fetch tarball
       └─ 写入 ~/.openclaw/skills/<slug>/SKILL.md
       └─ 记录 tracked slug → readTrackedClawHubSkillSlugs()
```

### 可借鉴点
1. **skill 是"无代码扩展"**——文档即扩展，**让非工程师也能扩展 agent 行为**。
2. **依赖声明式 + 安装声明式**：`requires` 描述"运行前置条件"，`install` 描述"如何满足前置条件"——`healthcheck` skill 可以巡检全部 skill 的就绪度。
3. **`allowed-tools` 收窄能力**：discord skill 只允许 `message` tool，防止 prompt injection 越权调用其它 tool。
4. **`user-invocable` 区分被动激活 vs 主动触发**：`gh-issues` 是用户 `/gh-issues` 显式调；`weather` 是 agent 根据上下文自动用。

---

## 3.7 Hooks（5 个内置 + plugin 扩展）

### 职责
监听 **command / session / agent / gateway / message** 五大类内部事件，可以**修改输出 / 注入提示 / 阻止执行**。类似 Claude Code 的 PreToolUse/PostToolUse，但事件分类更细。

### 关键文件
- `src/hooks/types.ts:1-50`：`HookSource` / `Hook` / `HookEntry` / `HookInstallSpec` / `HookInvocationPolicy`。
- `src/hooks/internal-hook-types.ts:1-22`：`InternalHookEvent` 数据契约。
- `src/hooks/internal-hooks.ts`：事件创建 + 分发。
- `src/hooks/policy.ts:1-60`：四源优先级表。
- `src/hooks/frontmatter.ts`：HOOK.md 解析（同 SKILL.md 格式）。
- `src/hooks/loader.ts`：扫描 + 加载。
- `src/hooks/bundled/`：5 个内置 hook —— `boot-md` / `bootstrap-extra-files` / `command-logger` / `compaction-notifier` / `session-memory`。
- `src/hooks/plugin-hooks.ts`：plugin 也可以注册 hook。

### 数据结构

```typescript
type InternalHookEventType = "command" | "session" | "agent" | "gateway" | "message";

interface InternalHookEvent {
  type: InternalHookEventType;
  action: string;            // e.g. "new", "reset", "stop", "tool-call"
  sessionKey: string;
  context: Record<string, unknown>;
  timestamp: Date;
  messages: string[];        // hook 可以 push 消息回给用户
}

type InternalHookHandler = (event: InternalHookEvent) => Promise<void> | void;
```

**四类 hook source**（按优先级）：

| Source | precedence | trustedLocalCode | 默认开启 | 可覆盖谁 |
|---|---|---|---|---|
| `openclaw-bundled` | 10 | yes | default-on | bundled |
| `openclaw-plugin` | 20 | yes | default-on | bundled + plugin |
| `openclaw-managed` | 30 | yes | default-on | 全部 |
| `openclaw-workspace` | 40 | yes | **explicit-opt-in** | workspace |

> workspace hook 默认要显式 opt-in——避免恶意 workspace 文件偷偷劫持 agent 行为。

### HOOK.md frontmatter（与 SKILL.md 类似但多 events 字段）
```yaml
---
name: command-logger
metadata:
  openclaw:
    events: ["command:new", "session:start"]
    export: default       # 导出名（默认 "default"）
    requires:
      bins: [...]
    install: [...]
---
```

### 调用链
```
host emits hook event
  └─ createInternalHookEvent({ type, action, sessionKey, context })
  └─ for each enabled hook matching events filter:
       └─ handler(event) → event.messages.push(...)
  └─ messages 合入 agent 的下一轮 user-visible 输出
```

### 可借鉴点
1. **事件类型 union + action 字符串**——五大类 + action 命名（`command:new`、`session:reset`、`agent:tool-call`）让 hook frontmatter 用 `events: ["command:new"]` 这种字符串数组就能精准订阅。
2. **四源优先级 + workspace explicit-opt-in**：安全 / 配置 / 兼容三种语义的混合策略。
3. **messages 数组让 hook 能"软干预"**——不破坏主流程的前提下追加提示，比 hard-block 更友好。
4. **hook 和 plugin 共享 frontmatter 解析器**（`parseOpenClawManifestInstallBase` / `requires` 结构）——跨子系统的 manifest 一致性。

---

## 3.8 Commands（130+ 子命令模块）

### 职责
实现 `openclaw <subcommand>` 的具体业务逻辑。**注意**：`src/commands/` 是**业务实现层**，`src/cli/` 是**入口/路由/解析层**。

### 关键文件
- `src/commands/*.ts`：130 个去重后的命令业务模块。
- `src/cli/program.ts`：commander 主 program 装配。
- `src/cli/command-catalog.ts`：命令目录。
- `src/cli/command-registration.ts` / `command-specs.ts`：注册框架。

### 数据结构 / 130 个命令分类

按前缀聚类后看分布（不完整列表）：

**Agent/Session 类**：
`agent` / `agents` / `agent-via-gateway` / `sessions` / `sessions-cleanup` / `sessions-display-model` / `sessions-table` / `session-store-targets`

**Auth/Onboard 类**：
`auth-choice` / `auth-choice-legacy` / `auth-choice-options` / `auth-choice-prompt` / `auth-token` / `chutes-oauth` / `onboard` / `onboard-channels` / `onboard-config` / `onboard-core-auth-flags` / `onboard-custom` / `onboard-custom-config` / `onboard-hooks` / `onboard-interactive` / `onboard-non-interactive` / `onboard-remote` / `onboard-search` / `onboard-skills` / `onboarding-plugin-install` / `provider-auth-guidance`

**Channel/Daemon 类**：
`channel-account-context` / `channels` / `daemon-install-auth-profiles-source` / `daemon-install-auth-profiles-store` / `daemon-install-plan` / `daemon-install-runtime-warning` / `daemon-runtime` / `node-daemon-runtime`

**Configure**：
`configure` / `config-validation` / `cleanup-plan` / `codex-runtime-plugin-install`

**Doctor**（45 个！）：
`doctor` + `doctor-auth` / `doctor-auth-flat-profiles` / `doctor-auth-legacy-oauth` / `doctor-auth-profile-config` / `doctor-bootstrap-size` / `doctor-browser` / `doctor-claude-cli` / `doctor-command-owner` / `doctor-completion` / `doctor-config-analysis` / `doctor-config-audit-scrub` / `doctor-config-flow` / `doctor-config-preflight` / `doctor-cron` / `doctor-cron-dreaming-payload-migration` / `doctor-cron-legacy-delivery` / `doctor-cron-payload-migration` / `doctor-cron-store-migration` / `doctor-device-pairing` / `doctor-format` / `doctor-gateway-auth-token` / `doctor-gateway-daemon-flow` / `doctor-gateway-health` / `doctor-gateway-services` / `doctor-heartbeat-main-session-repair` / `doctor-heartbeat-session-target` / `doctor-install` / `doctor-legacy-config` / `doctor-memory-search` / `doctor-platform-notes` / `doctor-plugin-manifests` / `doctor-plugin-registry` / `doctor-prompter` / `doctor-repair-mode` / `doctor-sandbox` / `doctor-security` / `doctor-service-repair-policy` / `doctor-session-locks` / `doctor-session-state-providers` / `doctor-session-transcripts` / `doctor-skills` / `doctor-state-integrity` / `doctor-state-migrations` / `doctor-ui` / `doctor-update` / `doctor-whatsapp-responsiveness` / `doctor-workspace` / `doctor-workspace-status`

**Gateway/Status/Health**：
`gateway-install-token` / `gateway-presence` / `gateway-status` / `health` / `health-format` / `status` / `status-all` / `status-json` / `status-json-command` / `status-json-payload` / `status-json-runtime` / `status-overview-rows` / `status-overview-surface` / `status-overview-values` / `status-runtime-shared`

**Other**：
`backup` / `backup-shared` / `backup-verify` / `commitments` / `dashboard` / `docs` / `export-trajectory` / `flows` / `message` / `message-format` / `migrate` / `model-picker` / `oauth-env` / `oauth-tls-preflight` / `random-token` / `reset` / `sandbox` / `sandbox-display` / `sandbox-explain` / `sandbox-formatters` / `setup` / `systemd-linger` / `tasks` / `tasks-json` / `text-format` / `uninstall`

### Slash 命令 vs CLI 命令

OpenClaw 同时有两套命令空间：

| 维度 | CLI 命令 | Slash 命令（agent 内） |
|---|---|---|
| 入口 | terminal `openclaw <cmd>` | chat `/command` |
| 注册 | `src/cli/program.ts` 装配 commander | `src/commands/` 业务 + plugin 通过 `api.registerCommand()` |
| 用户 | 运维 / dev / onboard | end-user 在会话中触发 |
| 典型 | `openclaw doctor` / `openclaw plugins install ...` | `/configure` / `/reset` / `/status` |

**重叠**：很多命令模块（`configure` / `status` / `reset` / `tasks`）同时被 CLI 入口和 slash 注册使用——核心实现共享，入口适配层不同。

### 可借鉴点
1. **doctor 子命令 45 个**：把"诊断/修复"做成同等公民——每个子系统（auth / cron / gateway / session / skills / plugin / state...）都有独立 doctor 入口，故障定位粒度极细。
2. **status 拆 7 个细分模块**（`status` / `status-all` / `status-json` / `status-json-runtime` / `status-overview-*`）：避免单一 status 命令成超大胖文件。
3. **commands/ 与 cli/ 严格分层**：业务实现不知道是被 CLI 还是 slash 调——便于复用。

---

## 3.9 CLI 入口层（src/cli/）

### 职责
**命令行入口的解析、路由、引导**。从 argv 到具体 command 的全部 wiring。

### 关键文件（180+ 个 .ts，挑骨架）
- `src/cli/program.ts`：commander 主入口装配。
- `src/cli/argv.ts` / `argv-invocation.ts` / `windows-argv.ts`：argv 解析。
- `src/cli/command-catalog.ts` / `command-bootstrap.ts` / `command-registration-policy.ts`：命令注册框架。
- `src/cli/command-secret-gateway.ts` / `command-secret-targets.ts` / `command-secret-resolution.*`：CLI 阶段的 secret 注入与遮蔽。
- `src/cli/completion-cli.ts` / `completion-fish.ts` / `completion-runtime.ts`：shell completion。
- `src/cli/plugins-cli.ts` / `plugins-cli.install.ts` / `plugins-cli.update.ts` / `plugins-cli.list.ts` / `plugins-cli.uninstall.ts` / `plugins-cli.search.ts` / `plugins-inspect-command.ts`：plugin 生命周期 CLI（覆盖 ClawHub 搜索、装、更、卸、列、查）。
- `src/cli/skills-cli.ts` / `skills-cli.format.ts`：skill 生命周期 CLI。
- `src/cli/hooks-cli.ts`：hook CLI。
- `src/cli/mcp-cli.ts`：MCP server 管理。
- `src/cli/gateway-cli.ts` / `gateway-rpc.ts`：gateway daemon 管理。
- `src/cli/daemon-cli.ts` / `cron-cli.ts` / `proxy-cli.ts` / `sandbox-cli.ts` / `secrets-cli.ts` / `security-cli.ts` / `system-cli.ts` / `tui-cli.ts` / `update-cli.ts` / `acp-cli.ts`：子系统 CLI 入口。
- `src/cli/qr-cli.ts` / `qr-dashboard.integration.test.ts`：device pairing 二维码。
- `src/cli/clawbot-cli.ts`：clawbot 子命令。
- `src/cli/profile.ts` / `respawn-policy.ts` / `run-main.ts`：进程生命周期（含 entry respawn）。

### 数据结构 / 调用链
```
$ openclaw plugins install @openclaw/xxx
  └─ src/cli/run-main.ts
       └─ src/cli/program.ts: build commander program
            └─ command-bootstrap.ts: 注入 doctor / plugins / skills / ...
                 └─ plugins-cli.ts: 'install' action
                      └─ plugins-install-command.ts: plan + record
                           └─ src/plugins/clawhub.ts: 校验 + 下载 + 解压
                                └─ src/plugins/install.ts: 落地 + commit config
```

### 可借鉴点
1. **plugins-cli 拆 7 个文件 + plugins-cli-test-helpers**：列表/搜索/装/卸/更/inspect/policy 各自独立模块——避免一个 1000 行的 cli command 文件。
2. **secret gateway**：CLI 阶段就把 `--api-key xxx` 拦截、不进 process.argv 上半区，避免泄进日志 / `ps aux`（`command-secret-targets.ts`）。
3. **completion-runtime 独立模块**：shell completion 不挂在主 program 装配里，**冷启动时不用加载全部命令**。
4. **runtime/test 双子模块**：很多 cli 文件配 `.runtime.ts` 后缀，把"实际跑命令副作用"和"声明命令骨架"切开——测试只 import 后者。

---

## 3.10 Wizard（首次设置向导）

### 职责
**首次安装/首次升级时的交互式 setup**。用 [@clack/prompts](https://www.npmjs.com/package/@clack/prompts) 风格的 TUI 引导用户配置 provider auth、gateway port、plugin 选择。也用于"重置"流程。

### 关键文件
- `src/wizard/setup.ts:1-80+`：主流程编排（auth-choice / gateway-config / plugin-config / migration-import / finalize）。
- `src/wizard/prompts.ts:1-40`：抽象 prompter 接口（`WizardSelectParams` / `WizardMultiSelectParams` / `WizardTextParams` / `WizardConfirmParams` / `WizardProgress`）。
- `src/wizard/clack-prompter.ts`：clack 实现。
- `src/wizard/setup.types.ts`：`WizardFlow` / `OnboardMode` / `QuickstartGatewayDefaults`。
- `src/wizard/setup.gateway-config.ts`：gateway 端口选择。
- `src/wizard/setup.plugin-config.ts` / `setup.official-plugins.ts`：plugin 选择 + 安装。
- `src/wizard/setup.secret-input.ts`：API key 等敏感输入（masked，不进 scrollback）。
- `src/wizard/setup.migration-import.ts`：从 Claude CLI / Hermes 等老配置迁移。
- `src/wizard/setup.completion.ts`：shell completion 安装。
- `src/wizard/setup.post-install-migration.ts`：升级版本时的 config 迁移。
- `src/wizard/setup.security-note.ts`：security note 弹窗 + 用户确认。
- `src/wizard/setup.finalize.ts`：写入 config 文件 + 触发 plugin install commit。
- `src/wizard/session.ts`：交互会话状态。

### 数据结构
```typescript
type SetupFlowChoice = WizardFlow | "import";  // 全新装 / 升级 / 重置 / 从已有配置迁移

type WizardSelectParams<T = string> = {
  message: string;
  options: Array<{ value: T; label: string; hint?: string }>;
  initialValue?: T;
  searchable?: boolean;
};

type WizardTextParams = {
  message: string;
  initialValue?: string;
  placeholder?: string;
  validate?: (value: string) => string | undefined;
  sensitive?: boolean;   // masked input, 不进 scrollback / transcripts / screenshots
};
```

### 调用链
```
$ openclaw setup  (或首启动自动)
  └─ src/wizard/setup.ts
       ├─ detectSetupMigrationSources()       // 检测可迁移源
       ├─ choose flow (quickstart / advanced / import / reset)
       ├─ setup.gateway-config           // gateway port
       ├─ setup.plugin-config / official-plugins   // 选 plugin
       ├─ setup.secret-input / auth-choice  // provider auth
       ├─ setup.security-note            // security 确认
       ├─ setup.completion               // shell completion 安装
       ├─ setup.finalize.ts              // commit config
       └─ commitConfigWriteWithPendingPluginInstalls()
            └─ replaceConfigFile + after-write hook
```

### 可借鉴点
1. **prompter 抽象接口 + clack 实现**——便于在测试 / 非 TTY / 远程场景注入 mock prompter。
2. **sensitive input 标记**：API key 输入显式 `sensitive: true`，prompter 实现保证"不进 scrollback、transcripts、screenshots"。
3. **`migration-import` 作为独立 flow**：从已有 Claude CLI / Hermes 配置一键迁移——降低用户迁移成本。
4. **finalize 用 `commitConfigWriteWithPendingPluginInstalls`**：把"plugin install 已下载但未生效"和"config 写盘"做成同一个原子事务——避免半装状态。
5. **配置写入走 `replaceConfigFile` + `allowConfigSizeDrop: true`**：升级时可以"瘦身"配置，去掉过期字段。

---

## 3.11 ClawHub：远程 skill/plugin registry

### 职责
**OpenClaw 生态的 npm-equivalent**——托管社区 skill 和 plugin 的中央注册中心，host 通过 HTTPS API 搜索 / 解析版本 / 下载 tarball / 校验完整性。

### 关键文件
- `src/infra/clawhub.ts`：HTTP client（fetch package detail / version / artifact / archive；spec 解析；compatibility 判定）。
- `src/plugins/clawhub.ts:1-100+`：plugin 安装链路（下载 + 校验 + `installPluginFromArchive`）。
- `src/plugins/clawhub-install-records.ts`：安装记录。
- `src/agents/skills-clawhub.ts`：skill 安装链路（`installSkillFromClawHub` / `searchSkillsFromClawHub` / `updateSkillsFromClawHub` / `readTrackedClawHubSkillSlugs`）。
- `src/cli/skills-cli.ts:6-15`：CLI 调用入口。
- `src/cli/plugins-cli.ts` 系列：plugin CLI 调用入口。
- `src/cli/plugin-registry.ts` / `plugin-registry-loader.ts`：本地 registry 缓存。
- `packages/plugin-package-contract/`：上架/下载两端共用的契约校验。
- `skills/clawhub/SKILL.md`：agent 自己也能用 `clawhub` CLI（套娃：clawhub 既是 host 装 skill 的来源、也是 agent 装 skill 的工具）。

### 数据结构
**ClawHub 错误码**（`CLAWHUB_INSTALL_ERROR_CODE`）：

```typescript
const CLAWHUB_INSTALL_ERROR_CODE = {
  INVALID_SPEC: "invalid_spec",
  PACKAGE_NOT_FOUND: "package_not_found",
  VERSION_NOT_FOUND: "version_not_found",
  NO_INSTALLABLE_VERSION: "no_installable_version",
  SKILL_PACKAGE: "skill_package",                   // 把 skill 当 plugin 装会被拒
  UNSUPPORTED_FAMILY: "unsupported_family",
  PRIVATE_PACKAGE: "private_package",
  INCOMPATIBLE_PLUGIN_API: "incompatible_plugin_api",
  INCOMPATIBLE_GATEWAY: "incompatible_gateway",
  MISSING_ARCHIVE_INTEGRITY: "missing_archive_integrity",
  ARCHIVE_INTEGRITY_MISMATCH: "archive_integrity_mismatch",
};
```

### 调用链
```
$ openclaw plugins install @author/x
  └─ src/cli/plugins-cli.ts
  └─ parseClawHubPluginSpec("@author/x[@version]")
  └─ fetchClawHubPackageDetail()           // GET registry meta
  └─ resolveLatestVersionFromPackage()
  └─ fetchClawHubPackageVersion()          // GET version detail
  └─ satisfiesPluginApiRange() + satisfiesGatewayMinimum()  // compat
  └─ fetchClawHubPackageArtifact()         // 解析下载 URL
  └─ downloadClawHubPackageArchive()       // 下 tarball
  └─ normalizeClawHubSha256Integrity()     // 校验 SHA256
  └─ loadZipArchiveWithPreflight()         // archive limit + safety scan
  └─ installPluginFromArchive()            // 解压 + 落地 + 注册
```

### 可借鉴点
1. **skill 和 plugin 用同一个 ClawHub 但严格区分**——`SKILL_PACKAGE` error code 防止把 skill 当 plugin 装。
2. **archive preflight**：`loadZipArchiveWithPreflight` 用 `DEFAULT_MAX_ARCHIVE_BYTES_ZIP` / `DEFAULT_MAX_ENTRIES` / `DEFAULT_MAX_EXTRACTED_BYTES` / `DEFAULT_MAX_ENTRY_BYTES` 防 zip bomb——这是社区 registry 必须做的安全垫。
3. **强制 SHA256 integrity**：`MISSING_ARCHIVE_INTEGRITY` / `ARCHIVE_INTEGRITY_MISMATCH` 让 host 验完整性，杜绝 MITM。
4. **compatibility 双闸**：`INCOMPATIBLE_PLUGIN_API`（SDK semver range）+ `INCOMPATIBLE_GATEWAY`（最小 host 版本）——和 `plugin-package-contract` 配合形成"包 publish 时校验、host install 时再校验一次"的双层。
5. **agent 也能调 ClawHub**：`skills/clawhub/SKILL.md` 暴露 `clawhub` CLI 给 agent，agent 可以自己探索/安装 skill——"agent 改造 agent"的扩展形态。

---

## 三、扩展性

### 总结表（跨子系统对照）

| 子系统 | 形态 | 注册入口 | 数量 | 信任级 | 沙箱 |
|---|---|---|---|---|---|
| Extension（bundled plugin） | npm 包 + manifest + TS entry | `definePluginEntry()` / `defineBundledChannelEntry()` | 133 | trustedLocalCode | 软（API facade + import 边界） |
| External plugin（ClawHub） | npm 包 + `openclaw.compat.pluginApi` | 同上 | n/a | 受 `plugin-package-contract` 闸 | 同 extension + archive preflight |
| Skill | SKILL.md（YAML frontmatter + Markdown） | 文件存在 + frontmatter 有效 | 53 | workspace 可控 | 强（只是文本，不跑代码） |
| Hook | HOOK.md + handler.ts | `events: ["command:new"]` 等 | 5 内置 | 四源优先级 + workspace opt-in | 软（同进程 Node） |
| Command | TS 模块 + commander option | `command-registration.ts` 或 `api.registerCommand()` | 130 | trusted | n/a |
| CLI 入口 | `src/cli/program.ts` 装配 | commander API | 180+ 模块 | trusted | secret gateway 隔离 |
| Wizard | TS 模块 + prompter 接口 | `setup.ts` 编排 | 单一流 | trusted | sensitive 标记 |

### 核心借鉴清单

1. **manifest-driven loading**：所有可扩展点（extension / skill / hook）都用"manifest + 实现分离"，host 在不 import 代码的情况下就拿到 capability metadata——冷启动可控、UI 静态可生成。
2. **subpath exports 作为契约面**：`@openclaw/plugin-sdk` 用 63 个 subpath 而不是单一 index，按 subpath 演进契约、treeshake 友好、废弃公告精准。
3. **runtime 注入解耦 Node 全局**：`time-runtime` / `secure-random-runtime` / `file-access-runtime` 让 plugin 不直连 `Date.now()` / `crypto` / `fs`——可测、可审计、可替换。
4. **plugin-package-contract 是独立包**：publisher / registry / host 三方共用同一 normalize/validate 实现，杜绝"registry 收了但 host 装不上"的不一致。
5. **声明式依赖 + 声明式安装**：skill / hook 的 frontmatter `requires.bins/anyBins/env/config` + `install[]` 让"前置条件"和"如何满足前置条件"都是数据，可被 `healthcheck` 巡检、被 setup wizard 自动修复。
6. **四源优先级模型**：hook 的 `bundled / managed / plugin / workspace` 四源 + workspace 默认 explicit-opt-in——把"安全 / 配置 / 兼容"三种语义压进同一张策略表。
7. **archive preflight + SHA256 + compat 双闸**：ClawHub 安装链路用四层防护（spec → version compat → archive limit → integrity hash），是社区 registry 的必备安全垫。
8. **doctor 45 子命令**：诊断/修复做成同等公民，每个子系统独立 doctor 入口——故障定位粒度极细，AI agent 可以靠 doctor 子命令做自愈。
9. **wizard prompter 抽象 + sensitive 标记**：UI 行为可注入测试 mock，敏感输入保证不进 scrollback/transcripts/screenshots。
10. **skill 是"无代码扩展"**：让非工程师扩展 agent 行为，`allowed-tools` 收窄能力面防止 prompt injection 越权。
11. **agent 也能用 ClawHub**：skill 形式把 `clawhub` CLI 暴露给 agent——agent 可以探索 / 安装自己缺的 skill，形成"agent 自我进化"循环。
12. **新版的 4 个独立包**（`plugin-sdk` / `plugin-package-contract` / `sdk` / `memory-host-sdk`）反映 v2026.5.14 的契约面成熟化——把"plugin 写法"、"plugin 包契约"、"客户端 SDK"、"memory host 子系统 SDK"四种契约严格分离，每种都能独立演进。

### 适合移植到我们项目的优先级

| 优先级 | 项 | 收益 |
|---|---|---|
| P0 | manifest-driven plugin loading + lazy activation | 冷启动可控、配置 UI 静态生成 |
| P0 | skill = Markdown + frontmatter 的"无代码扩展" | 让非工程师 / agent 自己扩展能力 |
| P0 | runtime 注入（time / random / file-access）解耦 Node 全局 | 可测、可审计 |
| P1 | plugin-package-contract 作为独立包 | publisher / registry / host 三方一致 |
| P1 | hook 四源优先级 + workspace explicit-opt-in | 安全与可扩展双兼顾 |
| P1 | doctor 子命令同等公民模式 | 故障定位粒度细，AI 可自愈 |
| P2 | ClawHub-style registry + archive preflight + integrity | 自建 registry 时直接抄 |
| P2 | wizard prompter 抽象 + sensitive 标记 | TUI 测试与安全 |
| P2 | sdk vs plugin-sdk 双层切分 | 跨进程客户端独立演进 |


---

# OpenClaw v2026.5.14 — 能力工具 / UI 多端 / 基础设施 与 运维

> 调研路径：`/tmp/openclaw-fresh/`（v2026.5.14）
> 范围：`src/tools/`、`src/web-*`、`src/*-generation/`、`src/realtime-transcription/`、`src/tts/`、`src/markdown/`、`src/terminal/`、`src/memory*`、`extensions/canvas/`、`extensions/browser/`、`extensions/memory-*/`、`ui/`、`apps/{macos,ios,android,shared}/`、`src/{cron,secrets,security,sessions,infra,logging,proxy-capture}/`。

---

## 四、能力工具

OpenClaw 在 v2026.5.14 把"工具能不能用"和"工具怎么跑"彻底解耦。**`src/tools/` 不实现任何具体工具**，而是定义 **ToolDescriptor 的元数据 / 可用性表达式 / 计划器 (planner)**；真正的工具实现散在三个层次：
1. **`extensions/<plugin>/`** —— 外部能力（canvas、browser、whatsapp、openai、anthropic、ollama …）作为 plugin 注册，**`openclaw.plugin.json` 的 `contracts.tools` 字段**声明它贡献了哪些工具名。
2. **`src/<domain>/` 顶层域** —— `web-search/`、`web-fetch/`、`image-generation/`、`media-understanding/`、`tts/`、`realtime-transcription/`、`markdown/`、`terminal/`、`interactive/` 是**域 runtime**（provider 选择、调用、归一化），不直接暴露成 agent tool，而是被某个 plugin（如 `image-generation-core`）或 core executor 引用。
3. **`src/<top>.ts` 顶层粘合层** —— 比如 `src/browser-lifecycle-cleanup.ts`，纯粹是生命周期 hook，不是工具本身。

下面按子系统拆。

---

### 4.1 `src/tools/` — 工具描述符 + 计划器（v2026.5.14 新拆细）

**职责**
为整个 runtime 提供"**工具元模型**"：每个工具用 `ToolDescriptor` 描述（name / title / description / inputSchema / outputSchema / owner / executor / availability）。runtime 在每次组装 agent 上下文时调 `buildToolPlan()`，根据当前 auth/config/env/plugin 状态算出**当前可见**的工具集合 + **被隐藏**的工具及其原因。

**关键文件**
- `src/tools/types.ts:1-97` —— 核心类型：`ToolOwnerRef`（core / plugin / channel / mcp 四种归属）、`ToolExecutorRef`（同构四种执行器）、`ToolAvailabilitySignal`（always / auth / config / env / plugin-enabled / context）、`ToolAvailabilityExpression`（递归 allOf / anyOf）、`ToolDescriptor`、`ToolPlan`。
- `src/tools/availability.ts:1-170` —— `evaluateToolAvailability()`，把表达式归约成 `ToolAvailabilityDiagnostic[]`（空数组 = 可见）。
- `src/tools/planner.ts:1-58` —— `buildToolPlan(descriptors, availability)`：先稳定排序 (`sortKey ?? name`)、断言名字唯一、过 availability、未通过的进 `hidden` 带 diagnostics、通过的必须有 `executor` 否则抛 `ToolPlanContractError(code:"missing-executor")`。
- `src/tools/descriptors.ts:1-11` —— `defineToolDescriptor` / `defineToolDescriptors`，只是 typed identity，**强制开发者落到统一类型上**而不是裸 object。
- `src/tools/protocol.ts:1-22` —— `toToolProtocolDescriptor()`，把内部 descriptor 投影成发到 LLM 的协议 JSON（剥离 owner / executor / availability 等"宿主元数据"，只留 name/description/parameters/annotations）。
- `src/tools/execution.ts:1-18` —— `formatToolExecutorRef()`，把 `ToolExecutorRef` 拍扁成 `"plugin:browser:browser"` 这种字符串 key，给日志 / 路由用。
- `src/tools/diagnostics.ts:1-13` —— `ToolPlanContractError` 单一错误类，code 枚举 `duplicate-tool-name | missing-executor`。

**数据结构**
```ts
ToolDescriptor = { name, description, inputSchema, owner, executor?, availability?, ... }
ToolOwnerRef    = { kind: "core" } | { kind: "plugin", pluginId } | { kind: "channel", channelId, pluginId? } | { kind: "mcp", serverId }
ToolExecutorRef = 同 owner 四种 kind，但带具体的 executorId / toolName / actionId
ToolPlan        = { visible: ToolPlanEntry[], hidden: HiddenToolPlanEntry[] }
```

**调用链**
1. Plugin 在 `openclaw.plugin.json` 声明 `"contracts": { "tools": ["canvas"] }`。
2. Plugin runtime 调 `defineToolDescriptor({ name:"canvas", owner:{kind:"plugin",pluginId:"canvas"}, executor:{kind:"plugin",pluginId:"canvas",toolName:"canvas"}, availability:{kind:"plugin-enabled",pluginId:"canvas"}, ... })`。
3. Agent 上下文组装时调 `buildToolPlan({ descriptors: allRegistered, availability: currentCtx })`。
4. planner 排序 → 去重 → 过 availability → 校验 executor → 返回 `ToolPlan`。
5. `toToolProtocolDescriptors(plan.visible)` 投影成 LLM 协议格式（OpenAI / Anthropic 协议层）。
6. LLM 选中某个工具后，runtime 通过 `formatToolExecutorRef(entry.executor)` 路由到具体的 plugin runtime / mcp server / channel action。

**可借鉴点**
- **availability 是表达式而非布尔**：`allOf` / `anyOf` 嵌套 + 6 种 signal kind，配合 `ToolAvailabilityDiagnostic` 把"为什么这个工具不可用"做成**结构化诊断**而不是"它没出现"。我们的功能开关（feature flag）可以借用这套表达式：`{ allOf: [{ kind:"plugin-enabled", pluginId:"x" }, { kind:"auth", providerId:"y" }] }`。
- **owner / executor 拆开**：所有权（谁贡献的）和执行（谁来跑）解耦——同一个工具名可以由 core 贡献但 plugin 执行。
- **planner 强制契约**：duplicate name / missing executor 直接 `throw`，不允许 silent drop。
- **协议投影**：内部模型 vs 协议模型用不同 type，发到 LLM 前过一道"信息脱敏"。我们做 API 文档/SDK 投影可以参考。

---

### 4.2 `src/web-search/` 与 `src/web-fetch/` — 搜索 vs 抓取

**职责**
两者都是 **"在多家 provider 之间路由 + 归一化"** 的薄运行时。`web-search` 解决"给关键词找网页"，`web-fetch` 解决"给 URL 拿正文"。共享一个底座 `src/web/provider-runtime-shared.ts`（凭据检查、env 读取、provider 解析）。

**关键文件**
- `src/web-search/runtime.ts:1-58` —— `resolveSearchConfig()`、`resolveWebSearchEnabled()`、`runWebSearch()`；导出 `ListWebSearchProvidersParams / RunWebSearchParams / RunWebSearchResult`。
- `src/web-search/runtime-types.ts` —— provider entry / tool definition 类型。
- `src/web-fetch/runtime.ts:1-60` —— 对称结构 `resolveWebFetchEnabled()` / `runWebFetch()`；通过 `tools.web.fetch` 配置路径读取。
- `src/web-fetch/content-extractors.runtime.ts` —— **抓回 HTML 后跑可读性提取**（与 `extensions/web-readability/` 对接）。
- `src/web/provider-runtime-shared.ts:1-?` —— 跨搜索/抓取的共享逻辑：`hasWebProviderEntryCredential`、`providerRequiresCredential`、`readWebProviderEnvValue`、`resolveWebProviderConfig`、`resolveWebProviderDefinition`。

**Provider 落点**：实际 provider 在 `extensions/` 里：`exa`、`brave`、`tavily`、`duckduckgo`、`searxng`、`perplexity`、`firecrawl`（fetch）、`web-readability`（fetch）等。每个 provider 注册时分别走 `resolvePluginWebSearchProviders` / `resolvePluginWebFetchProviders`。

**数据结构**
```ts
RuntimeWebSearchProviderEntry { providerId, envVars[], requiresCredential, getCredentialValue, ... }
RunWebSearchResult            { providerId, results: [{ url, title, snippet, ... }] }
```
fetch 多一层：`{ content, contentType, extractedText, links?, statusCode }`。

**调用链**
1. Agent 调 `web_search` 工具 → core executor 调 `runWebSearch({ query, providerId? })`。
2. `selectApplicableRuntimeConfig()` 合并 config 快照（用户配置 + runtime override）。
3. `resolveRuntimeWebSearchProviders()` 列所有候选 → `sortWebSearchProvidersForAutoDetect()` 优先选有凭据的。
4. 调具体 provider 的 `run()` → 归一化结果。

**可借鉴点**
- **搜索 vs 抓取分目录**：边界清晰，两个流量模型完全不一样（搜索是 API key 调用，抓取是真去访问目标站、要考虑 robots / readability / 代理）。
- **provider auto-detect 排序**：把"配好凭据的"排前面，没凭据的可见但不默认用 —— 用户体验和 fallback 兼顾。
- **共享 `provider-runtime-shared` 而不是继承**：用函数共享而不是类层级，TypeScript 友好。

---

### 4.3 `src/image-generation/` / `music-generation/` / `video-generation/` / `media-generation/` — 媒体生成

**职责**
"文生图 / 文生音 / 文生视频"运行时。和 web 域同构：一个核心 runtime + 多 provider plugin。

**关键文件**
- `src/image-generation/runtime.ts` —— 生成入口（参数归一、调 provider、产出文件）。
- `src/image-generation/openai-compatible-image-provider.ts` —— **共享 OpenAI 兼容协议**实现（fal / stability / openai-image 都走这套）。
- `src/image-generation/image-assets.ts` —— 产物落盘到 `tmp/` 工作区。
- `src/image-generation/normalization.ts` —— 模型 ref 解析（`provider/model:size:quality`）。
- `src/media-generation/runtime-shared.ts` —— **图/音/视频三域共享**的 catalog、capability、normalization 基类。
- `src/media-generation/catalog.ts` —— provider × model 的能力矩阵（支持哪些尺寸 / 格式 / 时长）。
- `src/music-generation/`、`src/video-generation/` 结构对称。
- Provider plugin：`extensions/fal/`、`extensions/runway/`、`extensions/minimax/`、`extensions/comfy/`、`extensions/elevenlabs/`（音）、`extensions/image-generation-core`、`video-generation-core`。

**调用链**
agent tool（如 `image_generate`）→ `image-generation/runtime.ts:generate()` → `provider-registry` 选 provider → provider `run()` → 拿到 base64/URL → `image-assets.ts` 写盘 → 返回 imageResult 给 agent。

**可借鉴点**
- **`media-generation/` 作为三域共享基**：避免三遍写 catalog/capability/normalization。
- **OpenAI 兼容协议作为默认 provider 形态**：对接新 provider 只要遵循协议就零代码。
- **catalog 是数据**：能力矩阵（哪个模型支持哪个尺寸）作为静态数据而不是代码分支，UI 可直接渲染。

---

### 4.4 `src/realtime-transcription/` — 实时转写（流式）

**职责**
对接 Deepgram / Azure Speech / ElevenLabs / SenseAudio 等**双向 WebSocket** 实时 ASR provider。一个 session 管一路上行 audio chunk + 下行 transcript event 流。

**关键文件**
- `src/realtime-transcription/websocket-session.ts:1-60` —— `RealtimeTranscriptionWebSocketTransport` 抽象 + `createRealtimeTranscriptionWebSocketSession()` 工厂。封装：连接 → ready 握手 → 发音频 → 收事件 → reconnect/backoff → 关闭。
- `src/realtime-transcription/provider-registry.ts` —— provider 注册表（同 image/web 模式）。
- `src/realtime-transcription/provider-types.ts` —— `RealtimeTranscriptionSession` / `Callbacks`（`onTranscript`、`onError`、`onClose`、`onReady`）。
- Provider plugin：`extensions/deepgram/`、`extensions/azure-speech/`、`extensions/senseaudio/`、`extensions/elevenlabs/`、`extensions/inworld/`、`extensions/speech-core/`。

**数据结构**
```ts
RealtimeTranscriptionWebSocketSessionOptions = {
  callbacks: { onReady, onTranscript, onError, onClose },
  url: string | (() => Promise<string>),  // 支持动态签名 URL
  headers: ... ,                           // 动态认证
  sendAudio: (audio: Buffer, transport) => void,  // provider 自己决定 framing
  parseMessage?: (Buffer) => Event,        // provider 自己解协议
  maxQueuedBytes: 2MB,
  connectTimeoutMs: 10s,
  maxReconnectAttempts: 5,
  reconnectDelayMs: 1000,
}
```

**集成方式（流式 audio in/out）**
1. 上游（手机 mic / 设备 pair）通过 gateway WebSocket 把 PCM/Opus chunk 推给 node host。
2. node host 调 `createRealtimeTranscriptionSession({ providerId })` → 工厂走 provider plugin 拿到 sessionFactory。
3. provider 内部 `new WebSocket(realtimeUrl, headers)` → `readyOnOpen ? markReady() : 等 provider 协议 "ready" 事件`。
4. 收到 audio chunk → `session.sendAudio(buffer)` → provider 的 `sendAudio()` 按自家协议 framing 写入 ws。
5. provider ws 推 `transcript` JSON → `parseMessage` 解 → `callbacks.onTranscript({ text, isFinal, ... })`。
6. 上层把 transcript event 再走 gateway 广播到 UI/TUI。
7. 断连 → 内置 backoff + reconnect，最多 5 次；超 maxQueuedBytes 丢弃溢出 chunk。

**proxy-capture 集成**：`createDebugProxyWebSocketAgent()` + `captureWsEvent()` —— 开发模式下所有 ws 流量可以走调试代理捕获。

**可借鉴点**
- **transport 与 session 分层**：transport 只管 ws lifecycle + 队列，session 管业务事件回调，provider 只填 `sendAudio` / `parseMessage` 两个钩子。
- **URL / headers 支持 lazy async**：实时认证 token / 签名 URL 这种"每次连接都要新签"的场景天然支持。
- **proxy capture 一等公民**：把可观测性做进 transport 而不是事后塞，调试音频流极其重要。

---

### 4.5 `src/media-understanding/` 与 `src/link-understanding/` — 媒体/链接理解

**职责**
- `media-understanding`：把 attachment（图/音/视频）转成 LLM 能消化的形式 —— 图片直传、音频先 ASR 转文本、视频先抽帧 + ASR。
- `link-understanding`：聊天里出现 URL 时自动 fetch 摘要后注入上下文（**和 web-fetch 不同**：这是被动触发 + 自动应用）。

**关键文件**
- `src/media-understanding/apply.ts` / `apply.runtime.ts` —— 接管 attachment，输出"已经能塞进 prompt"的 normalized content。
- `src/media-understanding/audio-transcription-runner.ts` —— 音频文件走 ASR（非实时）。
- `src/media-understanding/openai-compatible-audio.ts` / `openai-compatible-video.ts` —— OpenAI 兼容的批量 ASR / video understanding。
- `src/media-understanding/provider-capability-registry.ts` —— 每个 provider 声明"能处理 image/audio/video/url 哪几类"。
- `src/link-understanding/detect.ts` / `runner.ts` / `apply.ts` —— 检测消息里的 link → 跑 fetch → 写回 context。

**可借鉴点**
- **理解 vs 生成对称**：`*-generation` 域和 `media-understanding` 域结构镜像，开发者心智一致。
- **link-understanding 独立于 web-fetch**：同一个底层抓取能力，但触发 / 注入语义完全不同 —— 不要复用一个 surface。

---

### 4.6 `extensions/canvas/` + A2UI — Agent 主动渲染 UI（重点）

**职责**
让 agent 在和用户聊天的过程中，**主动**在用户配对节点（手机 / 平板 / mac / iOS）的屏幕上**渲染交互式 UI**，并能接受用户操作回流。这是 OpenClaw 最有产品想象力的能力 —— "聊天 → 临时仪表盘"。

**关键文件**
- `extensions/canvas/openclaw.plugin.json` —— plugin 注册：`enabledByDefault:true`、`activation.onStartup:true`、`contracts.tools:["canvas"]`、配置 schema 支持 `host.enabled/root/port/liveReload`。
- `extensions/canvas/src/tool.ts:140-271` —— **`canvas` 工具实现**，**这是 agent 看到的唯一入口**。7 个 action：
  - `present` —— 在节点上打开 canvas surface（可指定 URL + placement x/y/w/h）。
  - `hide` —— 关闭。
  - `navigate` —— 跳转到指定 URL。
  - `eval` —— 在 canvas 里执行任意 JS（返回字符串结果），是个**逃生口**。
  - `snapshot` —— 截屏（PNG/JPEG，可指定 maxWidth/quality），结果落 tmp 文件 + 返回 imageResult 给 agent，agent 可以**"看到自己渲染的东西"**形成闭环。
  - `a2ui_push` —— **推送一段 A2UI JSONL**到 canvas，由 host 端 Web Component (`<openclaw-a2ui-host>`) 解析渲染。
  - `a2ui_reset` —— 清空当前 surface。
- `extensions/canvas/src/a2ui-jsonl.ts:1-60` —— **A2UI 协议 v0.8 / v0.9 实现**，5 种 action：`beginRendering` / `surfaceUpdate` / `dataModelUpdate` / `deleteSurface` / `createSurface`。`buildA2UITextJsonl(text)` 给出最小例子。`validateA2UIJsonl()` 在推送前校验。
- `extensions/canvas/src/capability.ts:1-25` —— **能力令牌**：每个 canvas URL 都用 `mintPluginNodeCapabilityToken()` 签出一个时限 token (`DEFAULT_PLUGIN_NODE_CAPABILITY_TTL_MS`)，scoped 到具体 plugin+node，URL 形态 `http(s)://host/plugin-node-cap/<token>/...`。
- `extensions/canvas/src/host/a2ui/index.html` —— **canvas host 页面**：背景渲染 + `<canvas id="openclaw-canvas">` 给 `canvas.eval` 用 + `<openclaw-a2ui-host>` Web Component 给 A2UI 用 + `a2ui.bundle.js`（由 `scripts/bundle-a2ui.mjs` 从 `host/a2ui-app/` 通过 rolldown 打）+ `window.__openclaw = { canvas, ctx, setStatus, setDebugStatusEnabled }` 是宿主 API。
- `scripts/bundle-a2ui.mjs` / `extensions/canvas/scripts/bundle-a2ui.mjs` / `copy-a2ui.mjs` —— 构建期把 host app bundle 进 plugin。

**A2UI 协议长什么样**
JSONL，一行一个 action object。看 `a2ui-jsonl.ts` 给的最小渲染文本的例子：
```jsonl
{"surfaceUpdate":{"surfaceId":"main","components":[
  {"id":"root","component":{"Column":{"children":{"explicitList":["text"]}}}},
  {"id":"text","component":{"Text":{"text":{"literalString":"Hello"},"usageHint":"body"}}}
]}}
{"beginRendering":{"surfaceId":"main","root":"root"}}
```
- `surfaceUpdate` 增量更新组件树（`Column` / `Text` / 其他控件，类似 Flutter 风格）。
- `beginRendering` 触发首帧渲染（必须在 surfaceUpdate 后）。
- `dataModelUpdate` 推数据绑定更新（`literalString` 之外可以走绑定路径）。
- `createSurface` / `deleteSurface` 多 surface 生命周期。
- 版本：`v0.8` 与 `v0.9` 同时支持（`A2UI_ACTION_KEYS` 联合）。

**Agent 怎么主动渲染 UI 给用户（完整数据流）**
1. agent 决定"我要给用户展示一个仪表盘"。
2. agent 调 `canvas { action: "present", node: "<user-phone>", url: "<some-template-or-blank>" }`。
3. `tool.ts:execute()` 走 `resolveNodeId()` 拿配对节点 ID → 通过 `callGatewayTool("node.invoke", { nodeId, command:"canvas.present" })` 发到 gateway。
4. gateway 经 WS 转给该 node 的 host runtime（`extensions/canvas/src/host/server.ts`），host 在节点设备上拉起一个 web view 加载 `index.html`，附带 capability scoped URL。
5. agent 再调 `canvas { action:"a2ui_push", jsonl: "..." }` 或 `{ jsonlPath: "tmp/dashboard.jsonl" }`。注意 `readJsonlFromPath()` 强制 `workspaceDir` 边界 (`isPathInsideRoot`)，防越权读盘。
6. gateway → node → host 把 JSONL 喂给 `<openclaw-a2ui-host>` Web Component → 组件解析协议、构建 / 增量更新组件树 → 渲染。
7. 用户在 UI 上点按 / 输入 → A2UI 反向事件通过同一条 ws 回流到 host → 走 `canvas.eval` 风格的反向回调 / dataModel 事件 → agent 在下一轮拿到 transcript 里的 user action。
8. agent 可调 `canvas.snapshot` **截屏看自己渲染的样子**，截图作为 image 输入回到 LLM —— 视觉闭环。

**安全 / 能力模型**
- 每个 surface URL 都是 capability scoped（`mintPluginNodeCapabilityToken` 默认 TTL，时间过了自动失效）。
- `a2ui_push` 的 `jsonlPath` 强制走 `realpath + isPathInsideRoot`，防 `../` 越界。
- `canvas.eval` 是逃生口但只能在 canvas surface 的 JS 上下文跑，访问不到 host 文件系统。

**可借鉴点（这一节是我们最关心的）**
- **JSONL 增量协议 vs 全量 DOM**：A2UI 一条 line 一个 action，天然支持流式渲染 + 增量更新，agent 可以**边想边推**而不是攒完一个完整 UI 树再发。
- **声明式组件 + dataModelUpdate**：UI 结构和数据分两个 update 流，agent 可以先建骨架再填数据。
- **agent 能看自己渲染的画面**：`snapshot` 把视觉反馈回流给 agent —— 这是 a11y tree 之外**真正的视觉 ground truth**，对调试 + 自我纠错极有价值。
- **capability scoped URL**：每个 surface 一个一次性 token，泄漏一个 URL 也只能看到那一个 surface 那一段时间，不像传统 session cookie 一次泄漏全玩完。
- **统一工具入口 (`canvas`) 多 action**：把 7 个动作合并成一个工具 + `action` 字段，LLM 的 tool list 不被淹没，但每个 action 的输入 schema 仍然清晰（`tool.ts` 用 flattened schema + 运行时 per-action 校验）。
- **host 页面 ≠ 业务页面**：`index.html` 只负责拉起 a2ui-host 这个 Web Component + 提供 `window.__openclaw` 给手动 `canvas.eval` 用，业务 UI 完全由 A2UI 协议驱动 —— 这意味着我们要做"agent 主动渲染"不需要预建大量页面，**渲染描述本身就是页面**。

---

### 4.7 `src/tts/` — 文本到语音

**职责**
TTS provider 注册 + 文本预处理 + 自动模式（auto-tts 根据消息类型决定是否朗读）。

**关键文件**
- `src/tts/tts-core.ts` —— 朗读入口。
- `src/tts/openai-compatible-speech-provider.ts` —— OpenAI 兼容 provider 基类。
- `src/tts/provider-registry.ts` —— provider 注册。
- `src/tts/tts-auto-mode.ts` —— 自动模式策略。
- `src/tts/directives.ts` —— 文本里嵌入 `[pause 1s]` `[voice alex]` 等指令的解析。
- `src/tts/prepare-text.ts` —— markdown 去格式、emoji 处理、缩写展开。
- `src/tts/status-config.ts` —— 朗读状态机配置。
- Provider plugin：`extensions/elevenlabs/`、`extensions/azure-speech/`、`extensions/tts-local-cli/`、`extensions/talk-voice/`、`extensions/speech-core/`、`extensions/inworld/`。

**可借鉴点**
- **directives 内嵌**：让 LLM 直接在文本里写 `[pause]`，比另开 SSML 参数低摩擦。
- **prepare-text 单独抽出**：markdown / code / emoji 三种"看了能读出来的"和"看了不能读的"分离处理。

---

### 4.8 `src/markdown/` — Markdown IR + 渲染感知分块

**职责**
为 TUI、UI、TTS 三种消费方提供一套 markdown 处理。不是一个新的 markdown 解析器，而是一套**渲染感知的中间表示 (IR)**。

**关键文件**
- `src/markdown/ir.ts` —— IR 节点定义（block / inline / table / code / blockquote / list 嵌套）。
- `src/markdown/render.ts` —— IR → text/ansi 渲染。
- `src/markdown/render-aware-chunking.ts` —— **流式 chunk 时不要切断 code fence / table / 列表**，这是和普通 markdown 库的核心差异。
- `src/markdown/fences.ts` / `tables.ts` / `code-spans.ts` / `frontmatter.ts` —— 子结构。

**可借鉴点**
- **流式 chunking 不切断结构**：LLM 流式输出时，每个 chunk 都要先过 ir 检查"现在是不是在 code fence/table 内部"，是的话推迟渲染。我们的 markdown 流式渲染如果还在用"逐字 push 到 markdown-it" 的方式可以参考。

---

### 4.9 `src/terminal/` — 嵌入式终端

**职责**
给 agent 提供一个**持久化的本地 shell 会话**（不同于一次性 `bash -c`），支持流式 stdout/stderr、信号、ANSI、cd 状态保持。

**关键文件**
- `src/terminal/` 下完整 PTY 抽象（与 `extensions/openshell/` 配合）。

**可借鉴点**
- **持久 shell vs 一次性命令**：agent 多步骤调试时（`cd`、`source env`、`python repl`）极其有用，一次性命令没法跨调用保状态。

---

### 4.10 `src/interactive/` — 交互 payload

**职责**
聊天消息里嵌入"待用户操作"的负载（按钮、表单、确认请求），UI/TUI 渲染成可交互的 widget。

**关键文件**
- `src/interactive/payload.ts` —— 负载类型 + 序列化。

**可借鉴点**
- 和 A2UI 互补：A2UI 是 agent 主动渲染整张 UI 在另一个 surface，interactive payload 是**就地**在聊天流里塞一个 widget。两种场景都要支持。

---

### 4.11 `src/browser-lifecycle-cleanup.ts` + `extensions/browser/` — 浏览器

**v2026.5.14 重要变化**：顶层 `src/browser/` 目录**已不存在**，浏览器实现完全迁到 `extensions/browser/` plugin。

**职责**
- `extensions/browser/`（plugin 全量实现，CDP 操控、profile 管理、bridge、doctor、record、tool runtime）。
- `src/browser-lifecycle-cleanup.ts` —— **唯一留在 core 的部分**：当 session/agent lifecycle 结束时，best-effort 关掉这个 session 关联的所有浏览器 tab，避免泄漏。它通过 `closeTrackedBrowserTabsForSessions()` 调 plugin-sdk 暴露的 maintenance API（`src/plugin-sdk/browser-maintenance.ts`），core 不直接持有 browser 引用。

**关键文件**
- `src/browser-lifecycle-cleanup.ts:1-32` —— 入口函数 `cleanupBrowserSessionsForLifecycleEnd({ sessionKeys, onWarn, onError })`，包一层 `runBestEffortCleanup` 保证错误不打断主流程。
- `extensions/browser/runtime-api.ts` —— 公开 API：`browserAct/Snapshot/Navigate/Screenshot/Doctor/Profiles/...` ~50+ 个动作。
- `extensions/browser/src/browser-tool.actions.ts` / `browser-tool.schema.ts` / `browser-tool.runtime.ts` —— agent tool 的统一入口和 schema。
- `extensions/browser/src/browser-cdp.ts` —— CDP 实现。

**可借鉴点**
- **大组件 plugin 化**：浏览器这种"很重 + 可独立"的组件完全外移 plugin，core 只保留生命周期 hook。我们的"重组件 vs 核心"边界划分可以参考 —— 默认 plugin、core 只放 cleanup/hook。

---

### 4.12 `src/memory/` + `src/memory-host-sdk/` + `packages/memory-host-sdk/` + memory extensions —— 记忆体系

**职责**
长期记忆 / 知识检索 / 多模态向量库。v2026.5.14 拆得**很细**：
- `src/memory/` —— 极简，**只有** `root-memory-files.ts`（路径常量、`AGENTS.md` / `CLAUDE.md` / `OPENCLAW.md` 的查找路径）。
- `src/memory-host-sdk/` —— **runtime 内嵌**的 host SDK：dreaming（后台整理）、engine-qmd（query-merge-dedupe）、engine-storage、multimodal、query、secret、status、events。
- `packages/memory-host-sdk/` —— **对外发布**的 npm 包（`@openclaw/memory-host-sdk`），exports：`runtime` / `engine` / `engine-foundation` / `engine-storage` / `engine-embeddings` / `engine-qmd` / `multimodal` / `query` / `secret` / `status`。**src 和 packages 是镜像**（src 里是 host 内部用的版本，packages 是 plugin 集成时引用的版本）。
- `extensions/memory-core/` —— core memory manager plugin（runtime-api、manager-runtime）。
- `extensions/memory-lancedb/` —— **LanceDB 向量库实现**。
- `extensions/memory-wiki/` —— wiki/knowledge 视图。
- `extensions/active-memory/` —— **主动记忆**（运行时自动注入相关记忆到 prompt，不是被动 query）。

**关键文件**
- `src/memory-host-sdk/dreaming.ts` —— 后台"做梦"任务（重新整理/总结/去重记忆）。
- `src/memory-host-sdk/engine-qmd.ts` —— Query-Merge-Dedupe 引擎。
- `src/memory-host-sdk/multimodal.ts` —— 多模态记忆（图/音/视频）。
- `extensions/memory-lancedb/lancedb-runtime.ts` —— LanceDB 集成。
- `extensions/active-memory/index.ts` —— 主动记忆 plugin。

**可借鉴点**
- **runtime 内置 SDK + 同名 npm 包**：core 内部 import 走 `src/memory-host-sdk/`，外部 plugin 走 `@openclaw/memory-host-sdk`，两边类型一致但 build target 不同。
- **dreaming 后台整理**：明确把"在线 query"和"离线整理"分两个 pipeline，避免在线路径变重。
- **active memory**：被动 query 用 tool，主动 inject 用 hook，两种角色拆开。

---

### 4.13 工具组织 vs extension 注册（v2026.5.14 设计决策）

**关系图**
```
src/tools/                      纯元模型 + planner，无具体工具
src/<domain>/                   域 runtime（web-search/image-gen/realtime-trans...），不暴露工具
extensions/<plugin>/            通过 openclaw.plugin.json.contracts.tools 注册工具
   ├─ canvas              贡献 "canvas" 工具，实现在 src/tool.ts
   ├─ browser             贡献 "browser" 工具，调 extensions/browser/src/*
   └─ image-generation-core  贡献图像生成工具，复用 src/image-generation/runtime.ts
```

agent 看到的 tool list = `buildToolPlan(allRegisteredDescriptors, currentAvailabilityCtx).visible`。

**`src/tools/` 是新引入的"工具元模型层"**，把 v2025 以前散落在各处的 tool 注册逻辑统一到一组类型 + 一个 planner，再让所有 plugin 走同一个出入口。**核心好处是"工具不可用"变成结构化诊断而非沉默失败**。

---

## 五、UI 与多端 App

OpenClaw 有四个并行的"端"：
1. **TUI**（`src/tui/`）—— 跑在用户当前 shell 里的终端 UI。
2. **Web UI / Control UI**（`ui/`）—— 浏览器里的控制台。
3. **macOS App**（`apps/macos/`）—— SwiftUI 原生。
4. **iOS App**（`apps/ios/`）—— SwiftUI 原生 + Share Extension + Live Activity Widget。
5. **Android App**（`apps/android/`）—— Kotlin + Jetpack Compose 原生。
6. **OpenClawKit**（`apps/shared/`）—— Swift Package，macOS/iOS 共用 ChatUI + Protocol + Kit。

所有端都连同一个 **Gateway WebSocket**（`src/gateway/`），协议层在 `src/plugin-sdk/agent-harness-runtime.ts` 等。

---

### 5.1 `src/tui/` — TUI

**职责**
基于 `@earendil-works/pi-tui`（**不是 ink**）渲染终端 UI。支持流式 markdown、tool execution 展示、聊天历史、命令补全、OSC8 超链接、本地 shell、会话切换。

**关键文件**
- `src/tui/tui.ts:1-30` —— 入口，import `TUI / Container / Loader / Text / Key / ProcessTerminal` 等 `@earendil-works/pi-tui` 原语。
- `src/tui/components/chat-log.ts` —— 聊天日志流式渲染。
- `src/tui/components/custom-editor.ts` —— 输入框（带补全、多行、历史）。
- `src/tui/components/markdown-message.ts` —— markdown 渲染（接 `src/markdown/`）。
- `src/tui/components/tool-execution.ts` —— 工具调用展示。
- `src/tui/components/filterable-select-list.ts` / `searchable-select-list.ts` / `selectors.ts` —— 列表/选择器。
- `src/tui/tui-stream-assembler.ts` —— 流式装配（和 markdown chunking 配合）。
- `src/tui/tui-overlays.ts` —— 弹层（设置 / 选模型 / 历史）。
- `src/tui/embedded-backend.ts` —— **TUI 内嵌的轻量 gateway**（不需要单独起 daemon）。
- `src/tui/gateway-chat.ts` —— 连远程 gateway 的模式。
- `src/tui/osc8-hyperlinks.ts` —— OSC8 超链接（终端可点击）。

**可借鉴点**
- **pi-tui vs ink 的选择**：React/ink 的虚 DOM 在大量流式 chunk 下性能不够，pi-tui 是命令式 + reactive cell，TUI 高频更新友好。
- **embedded backend / remote backend 双模式**：单机 `oc` 命令立刻能用，远程 `oc --gateway` 接上现有 daemon。

---

### 5.2 `ui/` — Web UI（Lit + Vite，**已确认**）

**职责**
浏览器/桌面 webview 里的控制台。**Lit 3.3.2 + Vite 8.0.12** 构建，**不是 React**。

**关键文件**
- `ui/package.json` 依赖：`"lit": "3.3.2"`、`"vite": "8.0.12"`，无 react/vue。
- `ui/vite.config.ts` —— Vite 配置。
- `ui/index.html` —— SPA 入口。
- `ui/src/main.ts` —— 启动。
- `ui/src/ui/app.ts` —— 主组件。
- `ui/src/ui/gateway.ts:415-460` —— **`GatewayClient` 类**，持有 `WebSocket`，`isConnected()` 看 `ws.readyState === WebSocket.OPEN`。`new WebSocket(url)` 创建连接，连接失败时 `formatBrowserWebSocketConstructorError()` 把浏览器安全策略错误（plaintext ws over https 等）翻译成结构化错误。
- `ui/src/ui/app-gateway.ts` —— gateway 包装。
- `ui/src/ui/app-polling.ts` —— 轮询补漏。
- `ui/src/ui/app-tool-stream.ts` —— 工具执行流式 UI。
- `ui/src/ui/app-render*.ts` —— 渲染各种 entity（avatar / usage / exec policy / helpers）。
- `ui/src/ui/canvas-url.ts` —— canvas surface URL 构造（连 capability scoped 路径）。
- `ui/src/ui/app-native-bridge.ts` —— **当 UI 跑在 macOS/iOS/Android webview 里**时，bridge 到 native 层（接 SwiftUI / Compose host）。

**和 Gateway 的通信**
- **唯一通道：WebSocket**（不用 SSE）。`new WebSocket(opts.url)` → 连接 → JSON 消息双向。
- 浏览器安全策略：HTTPS 页面禁止 `ws://`，错误信息引导用户用 `wss://` 或 loopback `http://127.0.0.1:18789`。
- 失败后有 polling fallback（`app-polling.ts`）保证状态不丢。

**可借鉴点**
- **Lit 而非 React**：编译产物小、运行时极轻、Web Component 天然兼容 webview 嵌入 native。我们如果做嵌入到 Tauri/iOS WKWebView/Android WebView 的控制台，Lit 是更好的选择。
- **gateway 单 ws 通道**：避免 SSE + WS 混用的复杂性；polling 只做补漏不做主路径。
- **native-bridge 抽象**：同一份 Web UI 跑浏览器 / macOS webview / iOS WKWebView / Android WebView，bridge 适配差异。

---

### 5.3 `apps/macos/` — macOS 原生 App

**职责**
SwiftUI 原生应用，跑本地 daemon + 提供菜单栏入口 + 设置面板 + 多 channel（Discord/Slack/...）配置。

**关键文件**
- `apps/macos/Package.swift` —— SwiftPM。
- `apps/macos/Sources/OpenClaw/` —— SwiftUI 应用主体（`OpenClawApp.swift` 入口、`AppState.swift` 状态、`AgentEventsWindow.swift` 事件流窗、`ChannelsSettings*.swift` channel 配置、`ConfigSettings.swift` 配置、`ContextRootMenu*.swift` 状态栏菜单、`CritterStatusLabel*.swift` 吉祥物状态条）。
- `apps/macos/Sources/OpenClawDiscovery/` —— Bonjour mDNS 发现其他 node。
- `apps/macos/Sources/OpenClawIPC/` —— 进程间通信。
- `apps/macos/Sources/OpenClawMacCLI/` —— `oc` CLI 接 macOS daemon。
- `apps/macos-mlx-tts/` —— 一个独立产物：Apple MLX 框架本地 TTS。

技术栈：**SwiftUI + SwiftPM + Bonjour + XPC/IPC**，无 web/electron。

**可借鉴点**
- **状态栏 + 多窗口** 是 macOS daemon-style app 标配，Discovery / IPC / CLI / 主 app 各一个 module。

---

### 5.4 `apps/ios/` — iOS 原生 App

**关键文件**
- `apps/ios/project.yml` —— XcodeGen 项目描述（不手维 `.xcodeproj`）。
- `apps/ios/Sources/OpenClawApp.swift` / `RootView.swift` —— SwiftUI 主体。
- `apps/ios/ActivityWidget/` —— **Live Activity / Dynamic Island** widget，显示 agent 实时状态。
- `apps/ios/ShareExtension/ShareViewController.swift` —— 系统 Share Sheet 集成（截屏/链接/文本直接发给 agent）。
- `apps/ios/fastlane/` —— 发布。
- `apps/ios/screenshots/` —— App Store 截图。

**可借鉴点**
- **Live Activity / Dynamic Island** 是手机端 agent 体验的关键 —— agent 后台跑任务时锁屏能看到进度。
- **Share Extension** 把系统级"分享给 agent"做成一等公民。

---

### 5.5 `apps/android/` — Android 原生 App

**职责**
Kotlin + Jetpack Compose。Foreground Service 跑 node，SecurePrefs 存凭据，Gateway bootstrap auth。

**关键文件**
- `apps/android/app/build.gradle.kts` —— `alias(libs.plugins.kotlin.compose)`、`compose = true`、`androidx.compose.bom`。
- `apps/android/app/src/main/java/ai/openclaw/app/` —— 主代码。
- 测试关键类（推断职责）：`SecurePrefs`（KeyStore 加密 prefs）、`SecurePrefsNotificationForwarding`（通知转发到 agent）、`GatewayBootstrapAuth`、`NodeForegroundService`（后台 node 守护）、`SessionKey`。
- `apps/android/benchmark/` —— Macrobenchmark。

**技术栈**：**Kotlin + Jetpack Compose + Foreground Service**，**不是 RN/Capacitor**。

**可借鉴点**
- **Foreground Service 跑 node**：移动端"agent 一直在"必须用 ForegroundService + notification，不能靠 WorkManager。
- **通知转发到 agent**：用户在手机收到任何通知 → 推给 agent → agent 可以联动回复。

---

### 5.6 `apps/shared/OpenClawKit/` — Swift 共享包

**模块**
- `OpenClawChatUI` —— SwiftUI 聊天组件（macOS + iOS 共用）。
- `OpenClawKit` —— core 客户端逻辑（gateway 连接、session 管理）。
- `OpenClawProtocol` —— gateway 协议 Swift 类型（与 TS 端 `src/gateway/` 对齐）。

**可借鉴点**
- **跨 macOS/iOS 抽 Swift Package**：避免两端 SwiftUI 重写聊天 UI，protocol 类型也共享一份。

---

### 5.7 `src/web/` 不是 WhatsApp Web

**澄清**：`src/web/` 现在**只有 2 个文件**：`provider-runtime-shared.ts` + 测试。这是给 `web-search` / `web-fetch` 共享的 provider runtime helper，**和"web UI"、"WhatsApp Web"完全无关**。WhatsApp 在 `extensions/whatsapp/` plugin 里。

---

## 六、基础设施与运维

OpenClaw 的 core 里运维相关代码量极大 —— `src/infra/` 一个目录就有 563 个文件，覆盖 approval / archive / backup / channel / clipboard / browser-open / brew / build-stamp / changelog / clawhub 等。这里只看用户问到的几个关键子系统。

---

### 6.1 `src/cron/` — Cron 任务调度

**职责**
让 agent 按时间表自动跑：日常 standup、监控、备份、定时拉数据。模型不是经典 cron daemon，而是**"isolated agent run"** —— 每次触发起一个隔离的 agent session 跑预设 prompt + 通过 delivery plan 把结果送达指定 channel。

**关键文件**
- `src/cron/schedule.ts` / `parse.ts` —— cron 表达式解析（`every`, `daily`, `at <time>`, 标准 5 段 cron）。
- `src/cron/active-jobs.ts` —— 活跃 job 列表 + 状态。
- `src/cron/service.ts` —— **核心调度服务**：上电时从持久化加载 job、按时间轴 arm timer、到点触发、防重入、catchup、failure alert、heartbeat、top-of-hour stagger（避免整点惊群）。
- `src/cron/isolated-agent.ts` + `src/cron/isolated-agent/` —— **每次触发起一个 isolated agent**，跑预设 prompt（"system event text"），完成后 reaper 清理。**核心隔离单元**。
- `src/cron/delivery.ts` / `delivery-plan.ts` / `delivery-preview.ts` —— 结果送达：把 agent 跑完的最后输出按 delivery plan 发到 channel（Discord/Slack/SMS/email/…）。**delivery 失败有 failure-notify 兜底通知到 audit channel**。
- `src/cron/heartbeat-policy.ts` —— **心跳 job**：周期性"我还活着"，集中失败通知策略。
- `src/cron/run-log.ts` / `run-diagnostics.ts` —— 每次 run 的日志和诊断。

**数据结构**
```ts
CronJob {
  id, identity (schedule-identity),
  schedule: CronExpression | EveryExpression | AtExpression,
  oneShot?: boolean,
  systemEventText: string,    // 给隔离 agent 的 prompt
  deliveryPlan: { channelId, target, format, ... }[],
  modelOverrides?: ...,
  authProfile?: ...,
  runTimeoutMs?: number,
}
CronJobRun {
  runId, jobId, startedAt, finishedAt?, status, lastError?, deliveryStatus[],
}
```

**任务模型**
- 三种 schedule：
  1. **标准 cron 表达式**（`0 9 * * *`）—— `service.issue-22895-every-next-run.test.ts` 等大量回归测试佐证。
  2. **`every N <unit>`**（`every 5 minutes`）。
  3. **`at <time>`**（一次性，`oneShot`，跑完自动 disable，见 `service.runs-one-shot-main-job-disables-it.test.ts`）。
- **isolated agent**：每次起一个新 session（独立 auth profile、独立 model override、独立 lane），跑完销毁，避免污染主 session。
- **catchup**：宕机重启后通过 `restart-catchup` 决定要不要补跑漏掉的 trigger。
- **rearm + dedup**：service 严格保证不会同一个 job 两个 timer (`prevents-duplicate-timers.test.ts`)。

**可借鉴点**
- **每次 cron 跑都是 isolated session** 而不是复用某个常驻 agent —— 干净的隔离 + 独立配额 + 失败不影响主 agent。
- **delivery plan 一等公民**：cron 触发 ≠ 直接发消息，中间隔一层"agent 跑 → 拿结果 → delivery plan 路由"，可以同一份结果发多个 channel + 失败补救。
- **heartbeat job** 内置：定期自检"调度系统本身还活着没"。
- **大量 issue-XXXXX-regression 测试**：每个 bug 一个测试文件名带 issue 号，永久挂着 —— 我们的回归测试可以直接复用这种命名习惯。

---

### 6.2 `src/secrets/` — 凭据存储

**职责**
集中管理 API key / OAuth token / 自托管服务密码。**核心抽象：`SecretRef`** —— 配置文件里**不写明文**，写一个引用，运行时按 ref 类型解析。

**关键文件**
- `src/secrets/credential-matrix.ts:1-19` —— **凭据矩阵**：`SecretRefCredentialMatrixDocument` 列出哪些 config path 接 secret ref、`secretShape` 是 `secret_input`（用户输入）还是 `sibling_ref`（引用另一个 ref）、`when` 决定按 api_key 还是 token 解释。
- `src/secrets/apply.ts` —— 写凭据到配置（包含 auth-profiles）。
- `src/secrets/configure.ts` / `configure-plan.ts` —— 交互式配置流程（先 plan 后 apply）。
- `src/secrets/audit.ts` —— **审计**：扫描配置 + auth profiles，发现没引用的凭据 / 引用但缺失的 / 弱凭据。
- `src/secrets/channel-secret-*.ts` —— 各 channel（Discord/Slack/Basic/TTS）的凭据定义。
- `src/secrets/channel-env-vars.ts` / `channel-env-var-names.ts` —— **每个 channel 声明它读哪些环境变量**作为凭据。
- `src/secrets/exec-resolution-policy.ts` —— **`exec` 类型 ref 的安全策略**（ref 可以是"跑某个命令拿凭据"，但运行时根据 policy 决定是否真跑，跑出 `getSkippedExecRefStaticError`）。
- `src/secrets/json-pointer.ts` / `path-utils.ts` —— config 路径操作。
- `src/secrets/legacy-secretref-env-marker.ts` —— 历史 env 标记的兼容。

**Secret ref kind**
推断主要类型：
- `secret_input` —— 用户输入的明文，存到 secure storage。
- `env` —— 引用环境变量名。
- `exec` —— 引用一个命令的 stdout（如 `op read op://...` 1Password CLI）。
- `sibling_ref` —— 引用 config 里另一个位置的 ref。

**存储**
- 凭据本体落在 `~/.config/openclaw/auth-profiles.json`（auth profile store）+ `openclaw.json`（config）。
- 平台 secure storage（macOS Keychain / iOS Keychain / Android EncryptedSharedPreferences "SecurePrefs"）在 native app 端。
- 桌面/服务器 fallback 到 文件 + 0600 权限。

**可借鉴点**
- **凭据矩阵作为正交文档**：每个 config path 接什么 ref / 什么时候必填 一处列全，新加 channel 时只要加一行矩阵 entry。
- **plan / apply 分离**：先生成"我要改哪些字段"的 plan，用户确认后 apply。配置型操作的安全模式。
- **exec ref + policy**：让用户接 1Password / pass / vault 等，但 runtime 决定是否真跑（防止被 prompt 注入诱导执行任意命令拿凭据）。
- **audit 内置**：`audit-deep-code-safety.ts` / `audit-extra.async.ts` 等做"我的配置安全吗"扫描。

---

### 6.3 `src/security/` — 运行时安全审计

**职责**
对 channel、exec、auth、DM 政策等做静态 + 动态审计。

**关键文件（举例）**
- `src/security/audit-channel.ts` + `audit-channel.collect.runtime.ts` —— channel 审计：DM 政策、source config、readonly 解析。
- `src/security/audit.deep.runtime.ts` + `audit-deep-code-safety.ts` —— 深度扫码。
- `src/security/audit-exec-safe-bins.test.ts` / `audit-exec-sandbox-host.test.ts` / `audit-exec-surface.test.ts` —— exec sandbox 审计。
- `src/security/audit-config-symlink.test.ts` —— config symlink 攻击检测。

**可借鉴点**
- **audit 作为一等的 runtime 子系统** 而不是事后脚本 —— 启动时就跑。

---

### 6.4 `src/sessions/` — 会话模型

**职责**
session id / session key / chat type / lifecycle events / 输入溯源 / model overrides。

**关键文件**
- `src/sessions/session-id.ts:1-5` —— **session id 形态强制 UUID v4**：`/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i`，`looksLikeSessionId()` 路由判定用。
- `src/sessions/session-key-utils.ts` —— session key 解析（agentId + sessionId 组合）。
- `src/sessions/session-lifecycle-events.ts` —— 生命周期事件总线（start / message / tool / end）。
- `src/sessions/transcript-events.ts` —— transcript 事件（assistant message / user message / tool execution）。
- `src/sessions/input-provenance.ts` —— **每条输入的来源**（哪个 channel / 哪个 user / 哪条 message id），用于审计和反 prompt-injection。
- `src/sessions/send-policy.ts` —— 决定 agent 何时可以"主动说话"（被动响应 vs 主动 push）。
- `src/sessions/model-overrides.ts` —— per-session 模型 override。
- `src/sessions/classify-session-kind.ts` —— main / sub / cron / oneshot 分类。
- `src/sessions/level-overrides.ts` —— per-session log level。

**持久化策略**
- session id 固定 UUID v4，跨重启稳定。
- transcript 事件流追加写日志（`src/cron/run-log.ts` 风格）。
- 状态机不写 session 本身，状态通过 `session-lifecycle-events` 派生。

**可借鉴点**
- **input-provenance** 每条输入带来源 —— 防 prompt injection 的最佳实践。
- **session-key 复合 (agentId + sessionId)**：多 agent 并行时 key 不撞。
- **send-policy** 让 agent 何时主动说话变成显式策略而不是隐式判断。

---

### 6.5 `src/infra/` — 通用基础设施 (563 文件)

**职责**
跨子系统的工具集。挑核心几类：

| 子类 | 文件示例 | 作用 |
|---|---|---|
| Abort / Backoff | `abort-signal.ts`、`backoff.ts` | 统一 AbortController + 退避 |
| Approval（用户审批 tool call） | `approval-handler-runtime.ts`、`approval-native-runtime.ts`、`approval-native-route-coordinator.ts` | agent tool 调用前的人工确认流程，支持 native 路由（macOS/iOS notification action） |
| Backup | `backup-create.ts`、`archive.ts`、`archive-path.ts` | 配置/记忆备份、归档 |
| Boundary | `boundary-path.ts`、`boundary-file-read.ts` | 路径越界检查（同 canvas 用的 `isPathInsideRoot`） |
| Bonjour | `bonjour-discovery.ts` | mDNS 发现其他 node |
| Brew / Binaries | `brew.ts`、`binaries.ts` | brew 安装、二进制定位 |
| ClawHub | `clawhub.ts`、`clawhub-spec.ts` | OpenClaw 注册中心 / 配置 hub |
| Channel | `channel-runtime-context.ts`、`channel-summary.ts`、`channels-status-issues.ts`、`channel-approval-auth.ts` | channel 公共 runtime |
| Errors | `errors.ts`、`non-fatal-cleanup.ts` | 错误归一化 + best-effort cleanup |

**可借鉴点**
- **approval 多路由**：核心审批流程同时支持 in-app、native notification、DM 三种触发方式。
- **boundary-path 强制**：所有"用户给路径"的入口都过同一个边界检查（canvas tool 也用）。
- **non-fatal cleanup**：lifecycle 结束时的清理函数都包一层 `runBestEffortCleanup` 保证错误不破坏主流程。

---

### 6.6 `src/logging/` 与 `src/logger.ts` / `src/logging.ts`

**职责**
分子系统的日志配置、控制台拦截、诊断日志（按 phase / payload / session 维度切片）。

**关键文件**
- `src/logging/config.ts` —— 日志配置 schema。
- `src/logging/console.ts` —— **拦截 stdout/stderr**（捕获第三方库 log，写到结构化通道）。
- `src/logging/console-capture.test.ts` / `console-timestamp.test.ts` —— 捕获 + 时间戳行为。
- `src/logging/diagnostic-runtime.ts` —— diagnostic 总入口。
- `src/logging/diagnostic-memory.ts` —— 内存里循环缓冲（不落盘的内部日志）。
- `src/logging/diagnostic-phase.ts` / `diagnostic-payload.ts` / `diagnostic-run-activity.ts` —— 分维度切片。
- `src/logging/diagnostic-session-attention.ts` —— 跨 session 注意力（哪个 session 最近出错最多）。
- `src/logging/state.ts` —— logging state singleton。
- `src/logger.ts` / `src/logging.ts` —— 顶层 facade。

**可借鉴点**
- **console capture**：第三方库的 `console.log` 也拿到 + 加时间戳 + 路由到 subsystem，开发联调 + 生产诊断都需要。
- **诊断三维度**（phase / session / activity）切片，定位"哪个阶段的哪个 session 最近在干啥"。

---

### 6.7 `src/proxy-capture/` — 调试代理

**职责**
开发模式下把所有出站 HTTP / WebSocket 流量经过本地代理，写到 blob store，方便回放 / 重现 bug。

**关键文件**
- `src/proxy-capture/proxy-server.ts` —— 本地代理服务器。
- `src/proxy-capture/ca.ts` —— 自签 CA（TLS 拦截）。
- `src/proxy-capture/env.ts` —— `createDebugProxyWebSocketAgent()`、`resolveDebugProxySettings()`，给 ws / fetch 注入代理 agent。
- `src/proxy-capture/blob-store.ts` —— 流量 blob 落盘。
- `src/proxy-capture/coverage.ts` —— 覆盖率统计（哪些 provider 的协议被抓过）。
- `src/proxy-capture/runtime.ts` —— `captureWsEvent()` 等 helper。

**可借鉴点**
- **可观测性内嵌而非外挂**：所有 ws / fetch transport 一上手就接代理 agent，不依赖 Charles/mitmproxy。开发 ASR/媒体生成这种"协议各家不一样"的场景救命。

---

### 6.8 其余补充

- `src/i18n/` —— 国际化。
- `src/config/` —— 配置 schema + 运行时 snapshot + 来源追踪（用户 / runtime override / 默认）。
- `src/process/` —— 子进程管理 wrapper。
- `src/compat/` —— 历史兼容层（迁移老配置）。
- `src/test-helpers/` / `src/test-utils/` —— 测试工具。
- `src/scripts/` —— 内部脚本运行时入口。
- `src/library.ts` —— 顶层 SDK 入口（对外发包 `openclaw` 的主 export）。

---

## 总结：对我们最有借鉴价值的 8 点

1. **Canvas A2UI 协议** —— JSONL 增量 + 声明式组件 + dataModel update + agent snapshot 视觉闭环 + capability scoped URL。**做 agent 主动渲染就是这套**。
2. **`src/tools/` 工具元模型 + 表达式 availability** —— 把"功能开关"做成结构化诊断。
3. **owner / executor 拆开 + 协议投影** —— 内部模型 vs LLM 协议模型分两份。
4. **Realtime transport 抽象**（transport + session + provider 三层）—— 流式音频 + auto-reconnect + proxy capture 一等。
5. **Cron isolated agent + delivery plan** —— 每次触发隔离 session，结果按 plan 路由送达。
6. **Secret ref 矩阵 + plan/apply 分离 + exec policy** —— 凭据管理范式。
7. **Lit + Vite + native bridge** —— Web UI 同时跑浏览器和 webview 的轻量栈。
8. **proxy-capture 内嵌** —— 开发期 ws/fetch 流量自动落盘可回放。
