# 架构

组件关系和数据流。维护边界、成熟度、公共契约和验证菜单见 [维护地图.md](维护地图.md)。

## 维护层级

Forge Hub 不是单一 bot，而是一组围绕 Claude Code 的本机通信组件。维护时先分清成熟度：

| 层级 | 组件 | 怎么维护 |
|------|------|----------|
| Stable Core | `hub-server/`、`hub-client/`、`forge-cli/`、`cli.ts`、`hub-test-harness/` | 主线能力。改动要保守，必须查调用方并跑对应验证 |
| 默认部署的 UI | `hub-dashboard/` | 已进入默认安装，但 UI 和接口仍在迭代 |
| Preview | `hub-app/` | macOS 原生客户端 preview，复用 Dashboard UI |
| Experimental | `forge-engine/` | 定时触发引擎，默认不部署、不注册，手动启用 |

## 公共契约

下面这些接口跨组件使用。改的时候要当公共接口处理：查调用方、补验证、同步文档。

| 契约 | 以什么为准 | 典型受影响方 |
|------|------------|--------------|
| 通道插件接口 | `hub-server/types.ts` 的 `ChannelPlugin` / `HubAPI` | 内置通道、第三方通道、plugin guide |
| 消息结构 | `InboundMessage`、`SendParams`、`SendResult` | Hub Server、通道插件、历史记录、MCP 工具 |
| HTTP / WebSocket API | `hub-server/endpoints.ts`（鉴权 + 分发）+ `hub-server/routes/`（业务逻辑） | Hub Client、CLI、Dashboard、Native Client |
| MCP 工具 | `hub-client/hub-channel.ts` | Claude Code session 里的 `hub_reply` / `hub_send_file` / `hub_send_voice` / `hub_replay_history` |
| 运行时状态文件 | `~/.forge-hub/state/`，见 [运行时状态.md](运行时状态.md) | allowlist、pending、历史、实例身份恢复 |
| 安全边界 | [SECURITY.md](SECURITY.md) + allowlist / token / owner 实现 | 所有远程通道、Dashboard、共享主机用户 |

## 顶层组件关系

```
┌──────────────────────────────────────────────────────────────────────┐
│                         Claude Code 实例                              │
│                                                                      │
│   ┌──────────────────────────┐    ┌──────────────────────────────┐  │
│   │ hub-channel               │    │ Bash                         │  │
│   │ (MCP channel)             │    │                              │  │
│   │                           │    │  forge-hub CLI (fh)          │  │
│   │ 4 个工具:                 │    │  (~/bin/fh)                  │  │
│   │  hub_reply                │    │                              │  │
│   │  hub_send_file            │    │  hub/allow/channels 等子命令 │  │
│   │  hub_send_voice           │    │                              │  │
│   │  hub_replay_history       │    │                              │  │
│   └──────────┬────────────────┘    └──────────┬───────────────────┘  │
│              │                                 │                     │
└──────────────┼─────────────────────────────────┼─────────────────────┘
               │                                 │
          WebSocket                           HTTP
          ws://localhost:9900/ws           localhost:9900
               │                                 │
               └────────────────┬────────────────┘
                                ▼
              ┌────────────────────────────────────────┐
              │        Hub Server（独立常驻进程）         │
              │        localhost:9900, launchd 保活      │
              │                                         │
              │   ┌──────────────┐  ┌────────────────┐ │
              │   │ router.ts    │  │ instance-      │ │
              │   │ 消息路由      │  │ manager.ts     │ │
              │   │ @mention     │  │ 实例生命周期    │ │
              │   └──────────────┘  └────────────────┘ │
              │   ┌──────────────┐  ┌────────────────┐ │
              │   │ channel-     │  │ state.ts       │ │
              │   │ loader.ts    │  │ 持久化状态      │ │
              │   │ 热加载插件    │  │                │ │
              │   └──────┬───────┘  └────────────────┘ │
              │          │                             │
              │   ┌──────┴────────────────────────┐    │
              │   │         channels/             │    │
              │   │   wechat.ts   telegram.ts     │    │
              │   │   feishu.ts   imessage.ts     │    │
              │   │   homeland.ts                 │    │
              │   └───────────────────────────────┘    │
              │                                         │
              │   运行时: ~/.forge-hub/                 │
              └────────────────────────────────────────┘
```

```
┌─────────────────────────────────────────┐
│   Native Client (hub-app/) · preview    │
│   Swift shell + WKWebView              │
│   会话管理 + Dashboard UI               │
│   JS Bridge ⇄ SessionScanner          │
└──────────────────┬──────────────────────┘
                   │ HTTP localhost:9900
                   ▼ Hub Server
```

## 主要组件

### 1. Hub Server（hub-server/）

**角色**：消息路由中心 + 通道管理器。独立常驻进程。

**入口**：`hub.ts`

**核心模块**：

| 文件 | 职责 |
|------|------|
| `hub.ts` | main 入口 + onMessage 路由中枢 + WebSocket 实例注册 |
| `endpoints.ts` | HTTP 鉴权 + 路由分发（业务逻辑在 `routes/` 子目录） |
| `routes/` | health、send、approval、instances、homeland、dashboard、search 七个路由模块 |
| `router.ts` | 消息路由逻辑：@mention 解析、主实例 fallback、通道订阅过滤、锁定拦截 |
| `approval.ts` | 远程审批 pending state + idLookup + TTL 清理 + 持久化恢复 |
| `rate-limit.ts` | per-instance `/permission-request` 速率限制 |
| `lock.ts` | 紧急锁定（panic button）、lock-phrase 匹配 |
| `tts.ts` | TTS 合成（text → mp3 → ogg）hook 协议 |
| `asr.ts` | ASR 统一入口 `resolveAsr`：plugin `asrTranscribe` → `FORGE_HUB_ASR_HOOK` → null 三级 fallback |
| `history.ts` | 分通道聊天历史（JSONL append + 读取） |
| `hub-state.ts` | Hub 级内存状态：当前配置、onMessage 回调、Dashboard presence、启动时间 |
| `channel-loader.ts` | 通道插件加载：auto-detect plugin shape，`FORGE_HUB_DEV=1` 时启用 fs.watch 热加载 |
| `channel-registry.ts` | 已加载 plugin 的 sendMap / metaMap 注册中心 |
| `channel-watchdog.ts` | 每 2 分钟扫 unhealthy 通道 stop+start 自动重启（按 `stoppedReason` 白名单） |
| `instance-manager.ts` | 实例生命周期管理：注册/注销、标签/描述持久化、在线列表 |
| `resolve.ts` | sender 昵称 → native ID 解析 + 订阅过滤 |
| `config.ts` | 路径常量、日志工具、hub-config.json 热加载、`formatUnauthorizedNotice`、`resolveFfmpeg` |
| `state.ts` | 通用持久化工具：`loadChannelState`/`saveChannelState` 读写 `state/` 子目录的 JSON |
| `types.ts` | TypeScript 接口定义（ChannelPlugin、消息格式等） |
| `channel-health.ts` | 通道自愈状态机（HEALTHY → RECOVERING → DORMANT），驱动出站 warning 注入 |
| `evidence.ts` | 未授权消息 evidence vault：`recordUnauthorizedEvidence` 落盘原始 payload 到 `state/_hub/evidence/` |
| `media-path.ts` | 媒体文件名清洗：`sanitizeMediaFileName` 去控制字符 / 路径分隔符 |
| `media-policy.ts` | 媒体大小策略：`DEFAULT_MEDIA_MAX_BYTES`（50 MB）+ `MediaSizeLimitError` |
| `message-auth.ts` | 入站消息鉴权工具：`getAuthSenderId`（iMessage auth_sender_id 等特殊通道的 sender 提取） |
| `message-queue.ts` | 消息投递队列（SQLite WAL `queue.db`）：无在线实例时入队，实例上线后 drain，24h 过期 |
| `process-utils.ts` | 子进程工具：`execFileAsync`、`spawn` 封装，供 lark-cli / ffmpeg 等调用 |
| `sanitize.ts` | 外来字段净化：Unicode zero-width / Bidi / Tag Block 检测 + entity-encode + truncate |
| `search.ts` | FTS5 全文搜索索引（可选）：SQLite `search.db` + trigram tokenizer 支持中文 |
| `security-event.ts` | 安全事件聚合：per-channel per-sender 限频（1 条/小时），多通道同时攻击合并告警，`fh hub security events` 可查 |
| `send-retry.ts` | 出站重试判定：`isNetworkError` 分类 + `SEND_RETRY_DELAY_MS` 常量 |
| `wechat-text-utils.ts` | 微信文本处理：`stripMarkdown` 去 Markdown 格式供纯文本通道使用 |
| `write-queue.ts` | 文件写入队列：`enqueueAppend` 串行化 JSONL append，防并发写坏 |
| `routes/search.ts` | `/search` 路由：FTS5 关键字搜索，支持 `q` / `channel` / `limit` / `since` 参数 |

**HTTP / WebSocket 端点**：

`endpoints.ts` 会把 `/api/*` 归一化到同名根路径，所以 Dashboard 可打 `/api/status`，脚本和旧调用方也可继续打 `/status`。开启 `HUB_API_TOKEN` 后，除公开 `/health` 和静态 Dashboard 文件外，受保护端点都需要 Bearer token、Dashboard HttpOnly cookie 或 WebSocket/SSE query token。

| 分组 | 端点 | 方法 | 用途 |
|------|------|------|------|
| 连接 / 鉴权 | `/ws` | GET→WebSocket | 实例连接，token 模式下用 `?token=` |
| 连接 / 鉴权 | `/health` | GET | 公开最小健康检查 |
| 连接 / 鉴权 | `/status` | GET | 详细 Hub 状态 |
| 连接 / 鉴权 | `/dashboard-auth` / `/dashboard-logout` | POST | Dashboard token 登录 / 清 cookie |
| 消息出站 | `/send` | POST | 发送文本；`channel="hub"` 时发实例间消息 |
| 消息出站 | `/send-file` | POST | 发送文件；本地路径必须在 `$HUB_DIR/sendable/`，HTTP(S) URL 除外 |
| 消息出站 | `/send-voice` | POST | 发送语音；非微信通道由 Hub 先 TTS→ogg |
| 历史 / 实例 | `/history` | GET | 分通道聊天历史，支持 `limit` / `since_ts` |
| 历史 / 实例 | `/instances` | GET | 在线和已知实例列表 |
| 历史 / 实例 | `/channels` | GET | 已加载通道 metadata |
| 实例管理 | `/set-tag` / `/set-name` | POST | 设置实例 tag（`set-name` 为兼容别名） |
| 实例管理 | `/set-description` | POST | 设置实例描述 |
| 实例管理 | `/set-channels` | POST | 设置实例通道订阅 |
| 实例管理 | `/set-summary` | POST | 设置在线实例工作摘要 |
| 审批 | `/permission-request` | POST | hub-client 转发远程审批请求，登记 pending 并推到 `approval_channels` |
| 审批 | `/pending` | GET | 列出挂起审批 |
| 审批 | `/pending/<id>` | DELETE | 手动清 stale pending |
| 审批 | `/pending/<id>/approve` / `/deny` / `/dismiss` | POST | Dashboard 一键批准 / 拒绝 / 解除挂起（dismiss 会向 CC 发 deny 通知） |
| 本地 Dashboard | `/overview` | GET | Dashboard 首屏聚合数据 |
| 本地 Dashboard | `/homeland/send` | POST | Dashboard / Homeland 本地入站消息 |
| 本地 Dashboard | `/homeland/stream` | GET→SSE | Homeland 消息 / 审批 / 状态事件流 |
| 本地 Dashboard | `/homeland/presence` | POST | Dashboard 活跃心跳 |
| 搜索 | `/search` | GET | FTS5 全文搜索（需 `search_index: true`），参数 `q` / `channel` / `limit` / `since` |
| 安全 | `/lock` / `/unlock` | POST | 锁定 / 解锁远程出站 |

### 2. Hub Client（hub-client/）

**角色**：MCP channel，跑在每个 Claude Code 实例里。Hub Server 和 Claude Code 之间的桥。

**入口**：`hub-channel.ts`（单文件）

**核心逻辑**：
1. 启动时读 `next-session.json`（或 legacy `next-name.txt`）获取实例身份
2. 如果有身份信息 → 通道模式（注册为 peer，接收消息推送）
3. 如果没有 → 自动探测 10s 内是否有 channel handler：有则进入临时通道模式；没有则进入工具模式（只暴露工具，不连 SSE/WebSocket）
4. 通道模式下通过 WebSocket 连接 Hub Server，接收消息 → 转为 MCP notification
5. 暴露 `hub_reply` / `hub_send_file` / `hub_send_voice` / `hub_replay_history` 四个 MCP 工具
6. 处理远程审批：拦截 `permission_request` notification → POST 到 Hub → Hub 推到审批通道 → 用户回复 → Hub WebSocket 推回 → 注入 `permission_response`

---

### 远程审批数据流（详细版）

```
Claude Code 产生敏感操作
   ↓ MCP notification
   claude/channel/permission_request { request_id, tool_name, description, input_preview }
   ↓
hub-channel（MCP notification handler）
   ↓ HTTP POST /permission-request  { ...params, instance: INSTANCE_ID }
   ↓
Hub Server
   ├─ 锁定状态？→ 503
   ├─ 发起 instance 不在线？→ 503（hub-client 交还本地弹窗）
   ├─ approval_channels 为空？→ 503
   ├─ request_id 冲突？→ 409
   ├─ genDisplayIdPair() → 生成独立的 yes_id, no_id（强绑定 verdict 语义）
   ├─ 登记 pending[request_id] { yes_id, no_id, from_instance, created_at, ... }
   ├─ idLookup[yes_id] = {request_id, "allow"}; idLookup[no_id] = {request_id, "deny"}
   └─ Promise.allSettled 并发推送到 approval_channels 里所有通道
      每个通道：
      ├─ resolveApprovalRecipient(channel) → 读 state/{channel}/allowlist.json 拿 approval_owner_id
      ├─ 构造提示文案（在 `routes/approval.ts` 里生成 `Claude wants to run ${tool_name}: ${description}`）
      └─ plugin.send({ to, content, type: "text" })
      任一通道成功 → 200 { success, pushed, failed }
      全失败 → 清除 pending + idLookup → 502

用户在某一通道回复 "yes {yes_id}" / "no {no_id}"
   ↓ 通道插件轮询到（先过通道层 allowlist 第一道）
   ↓ hub.pushMessage()
Hub Server onMessage()
   ├─ [第二道防线] validateAuthorizedSender(msg)——未授权 logError+告警+丢弃
   ├─ 前置条件: pendingPermissions.size > 0（没 pending 时所有 yes/no 都是聊天）
   ├─ LOOSE 正则：/^\s*(y|yes|n|no)\s+\S/i——宽松识别"想审批"
   │   └─ LOOSE 匹配但严格不过 → malformed ack + 列正确 id（防手滑）
   ├─ STRICT 正则：/^\s*(y|yes|n|no)\s+([a-km-z]{5})\s*$/i
   ├─ getAuthSenderId(msg) + isApprovalOwner(channel, authSenderId)——只认显式 approval_owner_id
   ├─ idLookup.get(displayId) → {request_id, expected_behavior}
   │   └─ 没找到 → ack "编号不存在" + 建议 Esc
   │   └─ behavior 和 verdict 不一致 → ack "yes/id 不一致" + pending 保持
   ├─ pendingPermissions.get(request_id) → 拿到 from_instance
   ├─ instance.send({ type: "permission_response", content: JSON({request_id, behavior}) })
   ├─ send 返回 dropped/backpressure/离线？→ pending 保持，ack 提示稍后重试
   ├─ send 成功后清理: pending + idLookup[yes_id] + idLookup[no_id]
   └─ sendApprovalAck(channel, fromId, "✅ 已批准 / ❌ 已拒绝")——用户即时 confirm

hub-channel（WebSocket handleMessage）
   ↓ case "permission_response"
   ├─ 解析 + 字段校验
   └─ mcpServer.notification({ method: "notifications/claude/channel/permission", params })
   ↓ MCP notification
Claude Code 解除等待，继续执行（或拒绝）
```

**TTL 兜底**：hub-server 内存里的 `pendingPermissions` Map 每 60 秒扫一次，对超过 240 分钟（4h）未响应的条目——**先给 client 发 `behavior="deny"` 通知让 Claude Code 的 MCP handler 停下，再 delete map**。用户永远不回复的请求不会永久占内存，client 也不会挂死。

**主人识别**：审批回复识别依赖 `state/{channel}/allowlist.json` 的 `approval_owner_id`。owner 必须仍在 allowlist 中；非 owner 发的 "yes xxxxx" 会被 `isApprovalOwner` 拦住，记入日志但不转发。

**两道防线**：
1. **通道层（第一道）**：wechat/imessage/feishu/telegram 收消息时查 allowlist，未授权消息直接拒收，记录 evidence / security event，并用固定模板告警；告警触发时同步落盘，`fh hub security` 可立即查看，不回显外部原文
2. **Hub onMessage（第二道）**：即使通道层漏了，onMessage 入口再做 allowlist 校验——未授权 → logError + 记录 security event + **丢弃原消息**（绝不路由给 Claude Code 实例）；只有进入审批回复识别时才额外收紧到显式 owner

两道共享同一份 allowlist 文件。`fromId=system` 的消息免疫第二道 check 避免递归。

**反馈回路**：Hub 对用户每次审批回复都有 ack 回到用户发的通道（用 `sendApprovalAck`）——`✅ 已批准 / ❌ 已拒绝 / ⚠️ 不一致请重试 / ⚠️ 编号不存在建议 Esc / ⏰ 已超时`。消除"用户回复后不知道 Hub 收没收到"的黑箱。

**Pull-model history**：WS ready 时 Hub 默认只自动推 5 条 per channel（避免 CC context 被淹），agent 需要更多时主动拉：
- 默认自动推 `auto_replay_count`（default 5）条，可 config 调
- agent 需要更多时调 `hub_replay_history(channel, limit, since_ts)` MCP tool 自己拉（返回作为 tool result，不 spam 其他 instance）
- 用户保底 `fh hub replay <ch> [count]` CLI
- `GET /history?channel=X&limit=N&since_ts=Z` 是共享数据源（支持增量）

**持久化 & 观测性**：
- `state/_hub/pending.json` 持久化 pending 队列，Hub 启动时 `loadPendingFromDisk()` 恢复 + idLookup 重建
- `audit.jsonl` 记录审批事件（如 `approval_pushed/granted/denied/timeout/mismatch/push_failed/restart_timeout/manual_resolve`）
- `fh hub pending` 看当前挂起、`fh hub approval-audit` 看历史
- `hub-config.json` 热加载（`fs.watch` + 200ms debounce），改 config 不需要 Hub 重启

**Self-test**：`fh hub self-test` spawn `hub-test-harness/harness.ts` → 后者 `mkdtemp` 700 权限目录 + spawn 独立 Hub 进程到 :9901 + 伪装 wechat 的 `test-injector.ts`（Unix socket 控制）→ 跑 8 个场景（happy allow/deny、mismatch、malformed、第二道防线、无关聊天、crash × 2）→ 清理。生产 Hub 不暴露 `/test` endpoint。

### 3. CLI（forge-cli/）

**角色**：统一管理工具。单文件 TypeScript 脚本。

**入口**：`forge.ts`（通过 `~/bin/fh` symlink，`forge-hub install` 自动建立）

**工作方式**：Hub 子命令 → HTTP 调 Hub Server API（`localhost:9900`），Bearer token 鉴权可选（`HUB_API_TOKEN` env）。

**常用命令**：`fh hub status` / `peers` / `channels` / `allow` / `revoke` / `listen` / `lock` / `unlock` / `pending` / `replay` / `self-test` / `health` / `fh engine list|remove|pause|log`（完整列表见 `fh --help`）。

### 4. Dashboard（hub-dashboard/）

**角色**：本地 Web UI。默认安装会部署源码到 `~/.forge-hub/hub-dashboard/`，执行 `bun run build` 生成 `dist/`，再由 Hub Server 静态托管。

**入口**：`src/App.tsx`，数据访问集中在 `src/api.ts`。开发模式下 Vite 通过 proxy 把 `/api/*` 和 `/ws` 转到 `localhost:9900`。

**当前定位**：默认部署的本地 UI，但仍在迭代。它依赖 `/overview`、`/pending`、`/homeland/*` 等接口；改这些 response 时要同时看 Dashboard、Native Client 和 CLI。

### 5. Native Client（hub-app/）

**角色**：macOS 原生客户端 preview。Swift shell 负责会话扫描、文件系统访问、终端控制；WKWebView 加载同一套 Dashboard UI；JS Bridge 连接两层。

**入口**：`hub-app/app/main.swift` + `hub-app/app/WebViewBridge.swift`。构建脚本 `hub-app/build.sh` 会先构建 dashboard，再打包 `Forge Hub.app`。

### 6. Forge Engine（forge-engine/）

**角色**：定时触发 agent 行动的实验引擎。独立 MCP channel，默认不在 `forge-hub install` 部署 / 注册范围内。

**入口**：`engine-channel.ts`。日常管理命令复用 `fh engine list|remove|pause|log`，但启用需要按 [forge-engine/README.md](forge-engine/README.md) 手动注册。

### 7. 安装入口（cli.ts）

**角色**：`forge-hub install / uninstall / doctor`。它会先把包内容复制成 `~/.forge-hub/package/` 安装包快照，再从这个快照部署 Hub Server、Hub Client、Dashboard runtime、launchd plist、MCP 注册和 `~/bin/fh` symlink。

**边界**：`cli.ts` 是安装管理入口，不是日常管理 CLI；日常命令在 `forge-cli/forge.ts`。

## 组件间通信

| 通信路径 | 协议 | 说明 |
|----------|------|------|
| Hub Client → Hub Server | WebSocket | 实例注册、消息收发、审批 |
| Hub Client → Hub Server | HTTP | MCP 工具调用（`/send*`）、`/permission-request`、`/history` |
| Hub Server → Hub Client | WebSocket | 消息推送、上下文注入、锁定通知 |
| CLI → Hub Server | HTTP | 管理操作（status / peers / send 等） |
| Dashboard → Hub Server | HTTP / SSE | `/overview`、审批操作、Homeland 本地消息流 |
| Hub Server → 通道平台 | 各平台 API | 微信(ilink HTTP) / TG(Bot API) / 飞书(lark-cli subprocess) / iMessage(AppleScript) |
| 通道平台 → Hub Server | 轮询 / WebSocket | 微信(HTTP轮询) / TG(长轮询) / 飞书(WebSocket订阅) / iMessage(SQLite轮询) |

## 通道插件接口

```typescript
interface ChannelPlugin {
  name: string;                        // 通道标识，如 "wechat"
  displayName?: string;                // 显示名，如 "微信"（缺省 loader 用 name fallback）
  aliases: string[];                   // 别名，如 ["wx"]
  capabilities: ChannelCapability[];   // "text" | "file" | "voice" | "image"——loader 强制过滤
  start(hub: HubAPI): Promise<void>;   // 启动轮询/监听；允许 throw（标插件加载失败）
  send(params: SendParams): Promise<SendResult>;   // 发送消息
  stop(): Promise<void>;               // 停止（loader 用 10s timeout 包住）
  isNativeId?(to: string): boolean;    // 可选：判断 to 是否已是 native ID（路由分流用）
  stoppedReason?: ChannelStopReason;   // 可选：插件自报停止原因（watchdog 决定是否自动重启）
  asrTranscribe?(audioPath: string): Promise<string | null>;  // 可选：通道自带 ASR
}
```

详细字段说明 + HubAPI 回调 + `InboundMessage` / `SendParams` / `SendResult` 见 [`hub-docs/channel-plugin-guide.md`](hub-docs/channel-plugin-guide.md) §2。

**已实现通道**：

| 通道 | 消息源 | 发送方式 | Bot 身份 | 能否代 user 回第三方 | 其他 |
|------|--------|----------|----------|----------------------|------|
| wechat | iLink HTTP 轮询（含 ASR text） | iLink sendmessage API（voice = mp3 附件文件，非原生语音条） | CC 以**插件形式**寄居在 user 个人微信中 | ❌ 架构决定不行——CC 看不到 user 本人微信会话 | 需 iLink token；CDN 媒体下载偶失败；silk voice 协议不通用 |
| telegram | Bot API 长轮询（45s + 看门狗） | Bot API sendMessage | 独立 TG bot 账号 | ❌ 默认单一；**可开放**给他人用（用户按需自配 allowlist） | 代理走 env `https_proxy` / `http_proxy` |
| feishu | lark-cli event consume（NDJSON 流） | lark-cli im +messages-send | 企业 bot（按飞书权限体系可扩展） | ❌ 默认单一；**权限体系允许扩展**到群/其他用户 | stdin pipe 断裂自动退出；需 `--as bot` |
| imessage | chat.db SQLite 轮询 | AppleScript | **user 本人 Apple ID**（唯一） | **✅ 唯一支持**——走本机 chat.db + AppleScript | 需 Full Disk Access；self-chat echo filter 防死循环 |
| homeland | Dashboard HTTP + SSE | 本地 SSE 推送到 Dashboard | 本机 Operator | ❌ 本地通道，不代回第三方 | 无外部凭证；浏览器入口仍需 allowlist `local://operator` |

**为什么这个矩阵重要**：agent 通过 hub 工作时，如果不理解通道语义差异，可能做出架构不成立的动作——比如试图让 CC 在微信上"代 user 回朋友消息"，或假设所有通道都能开放给第三方使用。README 的"通道能力"表是面向人类的精简版，这张表是面向 agent 的完整 matrix（含消息源 / 发送方式 / 特殊要求）。
