跳到主要内容

14 · 多入口如何共享一条状态

让终端、IDE、协议和聊天入口共享一条可追踪、可升级的 Agent 状态

本章任务

要回答的问题

同一任务从终端、IDE、API 和聊天入口进入时,怎样共享身份、状态、取消和事件流?

读完你能

  • 把入口适配与核心 Loop、Session 状态解耦
  • 统一 run、thread、user 与 channel 身份
  • 定义流式输出、取消、重连和版本升级语义
适合现在读
正在做 Agent 服务端、IDE 集成、多平台助手或协议适配的工程师
先修知识
理解 Session、事件流和入口鉴权
实践产物
一份入口适配器契约和多通道路由表
证据边界
共享状态不等于共享所有界面数据;隐私、能力和节奏仍应按通道裁剪

场景:用户在 IDE 启动一次重构,随后从 Slack 补充约束,又通过 API 点取消;网络重连后 IDE 重发最后一条消息。若每个入口维护自己的“当前状态”,系统可能同时存在两个 Run、一次失效取消和一条重复输入。

通过标准:thread、run、user、channel 身份分开建模;所有入口写入同一有序事件流;取消按 run_id 幂等;重连从游标继续而不是重放副作用;每个通道只渲染适合自己的状态视图。

四系统的入口拓扑:codex 多 binary、claude code 单 binary 多 subcommand、openclaw sub-CLI 加 plugins、hermes 17 platform adapter
同样是「用户怎么进来」,4 家的拆法跨度从 5 个 binary 到 17 个 adapter。

四家在多通道入口上的覆盖:

维度 CodexClaude CodeOpenClawHermes
主交互形态 TUI 默认加 exec 子命令脚本化REPL 默认加 -p、--print 脚本化gateway daemon 长跑加 sub-CLI clientgateway 单进程加 17 platform adapter
IDE 集成 codex stdio-to-uds(VSCode、Cursor 走 UDS)加 codex mcp-server(stdio MCP)--ide flag 自动 detect 加 IDE MCP。mcp serve 子命令开 MCP serverACP server(@agentclientprotocol/sdk)加 gateway WebSocket不主打 IDE。走 Webhook、API server、Mattermost、Matrix 等 ChatOps
MCP、远程协议 codex mcp-server (stdio) 加 codex app-server (axum HTTP+WS) 加 remote-controlmcp serve 子命令(stdio MCP)加 server 子命令(HTTP、Unix socket session server)ACP server(standardized agent control protocol)加 WebSocket gatewaymcp_serve.py(MCP server)加 API_SERVER platform adapter 加 Webhook
聊天平台覆盖 官方不主打,靠 ChatGPT cloud 加外部 wrapper官方不主打,靠 plugin、SDK 包装Telegram、Discord、WhatsApp 等以 channels-plugins 形式作为 npm 包安装17 个 platform adapter 直接内置(Telegram、Discord、Slack、Signal、Feishu、钉钉、企微、微信、Matrix、Mattermost、BlueBubbles、QQ、Email、SMS、Webhook、API server、HomeAssistant)
tui、cli 端 codex_tui crate 加 ratatuimain.tsx 里 ink 加 ratatui-like 渲染tui-cli 子命令加 ui-tui packages(独立 TUI 应用连 gateway)tui_gateway 包,连本地 gateway daemon
多通道入口这件事的工程化程度

Codex · 把每种调用模式都做成一个独立子命令,再用 UDS 桥接 IDE 和 daemon

Section titled “Codex · 把每种调用模式都做成一个独立子命令,再用 UDS 桥接 IDE 和 daemon”

入口的生命周期不同:TUI 要保持交互,CI 要尽快退出,IDE 需要双向流,daemon 则要长期持有状态。Codex 把它们拆成子命令,并用 stdio-to-UDS 连接 IDE 与 daemon。

它的解法是把这些场景都拆成 codex multitool 的子命令:一个二进制文件,但每个子命令都对应一种完全不同的入口模式。这种「multitool」模式跟 git 的设计哲学一致(一个 git 命令背后是 git-checkout、git-commit 等独立子命令),但每个子命令的代码组织又拆得很彻底,几乎等于多个独立 binary 共享一个分发器。

Codex codex/codex-rs/cli/src/main.rs:105-182 codex multitool 子命令一览:exec / review / mcp / mcp-server / app-server / remote-control / app / resume / fork / cloud / responses-api-proxy / stdio-to-uds / exec-server / features
#[derive(Debug, clap::Subcommand)]
enum Subcommand {
/// Run Codex non-interactively.
#[clap(visible_alias = "e")]
Exec(ExecCli),
/// Run a code review non-interactively.
Review(ReviewArgs),
/// Manage login.
Login(LoginCommand),
/// Manage external MCP servers for Codex.
Mcp(McpCli),
/// Start Codex as an MCP server (stdio).
McpServer,
/// [experimental] Run the app server or related tooling.
AppServer(AppServerCommand),
/// [experimental] Start a headless app-server with remote control enabled.
RemoteControl,
/// Launch the Codex desktop app.
#[cfg(any(target_os = "macos", target_os = "windows"))]
App(app_cmd::AppCommand),
/// Resume a previous interactive session.
Resume(ResumeCommand),
/// Fork a previous interactive session.
Fork(ForkCommand),
/// [EXPERIMENTAL] Browse tasks from Codex Cloud.
#[clap(name = "cloud", alias = "cloud-tasks")]
Cloud(CloudTasksCli),
/// Internal: relay stdio to a Unix domain socket.
#[clap(hide = true, name = "stdio-to-uds")]
StdioToUds(StdioToUdsCommand),
/// [EXPERIMENTAL] Run the standalone exec-server service.
ExecServer(ExecServerCommand),
// ...
}

每个子命令背后是一种独立的入口模式。最常见的几个分别是:裸跑 codex 进入由 ratatui 渲染的全屏 TUI(也就是用户日常对话的入口);codex exec 进非交互脚本模式,把一段 prompt 跑完就退出,适合 CI 流水线和自动化脚本;codex review 是上一个的特例,专门跑非交互的代码评审;

codex mcp-server 把当前进程变成一个 MCP 服务器(通过 stdio 跟外部通信),让 Cursor、Cline、Claude Desktop 这类支持 MCP 协议的客户端能把 codex 作为一个 agent 工具来调用;codex mcp 反过来管理外部 MCP server 的配置;

codex app-server 启一个用 axum 写的 daemon,同时支持 HTTP1 和 WebSocket,桌面 app 和远程客户端都通过它接入;codex resume / codex fork 接续或分叉一个历史会话。

里面最值得专门讲的是一个叫 codex stdio-to-uds 的隐藏子命令:它的存在解决的是 IDE 集成里一个棘手的工程问题。

想象一下 IDE 集成的一个常见冲突:很多工具入口由 IDE 启动子进程并通过 stdio 通信,但 agent 可能希望用常驻 daemon 保留会话状态、服务多个客户端或独立升级。

这两个需求是矛盾的:IDE 期望的是「一个子进程对应一个 agent 会话」,daemon 模型期望的是「一个 daemon 服务多个客户端」。

codex stdio-to-uds 的做法巧妙:它就是一个极轻量的桥进程。IDE 启动这个桥进程作为子进程,桥进程做的事情只有一件:把它从 stdio 收到的所有字节转发到一个 Unix domain socket 上,然后把 socket 上收到的所有字节写回 stdio。daemon 本身则监听在那个 socket 上。这样从 IDE 视角看,它跟一个普通的 stdio 子进程在通信。从 daemon 视角看,它在跟一个 socket 客户端通信。

daemon 可以随时重启升级(IDE 这边只会感受到一次重连),桥进程本身没有状态可言,挂掉重启也几乎没成本。

codex app-server 用 axum 同时支持 HTTP1 和 WebSocket(在 Cargo.toml 里把 axum 的 features 设置成 ["http1", "json", "tokio", "ws"]),所以同一个 daemon 既能做 RESTful 风格的 session 管理(适合无状态 client),也能做实时 WebSocket 推送(适合需要流式输出的 IDE)。

Claude Code · 把所有功能塞进一个 npm 包里,靠 commander 子命令树组织

Section titled “Claude Code · 把所有功能塞进一个 npm 包里,靠 commander 子命令树组织”

单包 CLI 的价值是安装和升级简单。Claude Code 用 commander 组织 REPL、脚本、MCP 和 server,并提供 —bare 让 CI 跳过交互行为。

Claude Code claude-code/src/main.tsx:971-1006 顶层 program 的核心 flag:--print / --bare / --ide / --mcp-config / --print(脚本模式);几乎所有功能都通过 flag 切换
.option('-p, --print', 'Print response and exit (useful for pipes). Note: The workspace trust dialog is skipped when Claude is run with the -p mode. Only use this flag in directories you trust.', () => true)
.option('--bare', 'Minimal mode: skip hooks, LSP, plugin sync, attribution, auto-memory, background prefetches, keychain reads, and CLAUDE.md auto-discovery. Sets CLAUDE_CODE_SIMPLE=1. Anthropic auth is strictly ANTHROPIC_API_KEY or apiKeyHelper via --settings (OAuth and keychain are never read). 3P providers (Bedrock/Vertex/Foundry) use their own credentials. Skills still resolve via /skill-name. Explicitly provide context via: --system-prompt[-file], --append-system-prompt[...].')
.option('--mcp-config <configs...>', 'Load MCP servers from JSON files or strings (space-separated)')
.option('--ide', 'Automatically connect to IDE on startup if exactly one valid IDE is available', () => true)
.option('--strict-mcp-config', 'Only use MCP servers from --mcp-config, ignoring all other MCP configurations', () => true)

子命令组(commander.command):

Claude Code claude-code/src/main.tsx:3894-3962 主要管理子命令:mcp serve / mcp list / mcp add-from-claude-desktop / server / auth / plugin / setup-token
const mcp = program.command('mcp').description('Configure and manage MCP servers')
mcp.command('serve').description(`Start the Claude Code MCP server`)
mcp.command('list').description('List configured MCP servers...')
mcp.command('add-from-claude-desktop').description('Import MCP servers from Claude Desktop (Mac and WSL only)')
mcp.command('reset-project-choices').description('Reset all approved and rejected project-scoped (.mcp.json) servers within this project')
program.command('server').description('Start a Claude Code session server')
.option('--port <number>', 'HTTP port', '0')
.option('--host <string>', 'Bind address', '0.0.0.0')
.option('--auth-token <token>', 'Bearer token for auth')
.option('--unix <path>', 'Listen on a unix domain socket')
.option('--workspace <dir>', 'Default working directory for sessions that do not specify cwd')
.option('--idle-timeout <ms>', 'Idle timeout for detached sessions in ms (0 = never expire)', '600000')
.option('--max-sessions <n>', 'Maximum concurrent sessions (0 = unlimited)', '32')

最常用的 5 个入口模式分别是:裸跑 claude 进 REPL(用 ink 库渲染的终端 UI),这是用户日常对话的入口。claude -p "..." 进所谓「print 模式」:把 prompt 跑完输出结果就退出,对 shell pipe 友好(可以 cat file | claude -p "summarise"),并且代码注释里有一句很重要的说明:这个模式会跳过 workspace trust 对话框,所以只应该在你信任的目录里用,避免恶意目录里的项目级配置被自动加载。

特别值得讲一下 claude --bare 这个极简模式:它存在的目的是给 CI 流水线和 SDK 调用提供一个「完全可控」的入口。一个常规 claude 启动时会做一大堆「自动化」行为:加载项目级 hooks、启动 LSP server、同步 plugin、生成 git attribution、加载长期记忆、在后台预取数据、读系统 keychain、自动发现并加载 CLAUDE.md 文件。这些都是为了让交互式开发者体验丝滑而存在的。

但在 CI 场景下,这些自动化行为反而是灾难:CI 跑得每次都不一样、kicking 出乎意料的副作用、读了不该读的环境信息。--bare 模式会一口气把这一整套自动化行为全部关掉,认证也只走最严格的两种方式(ANTHROPIC_API_KEY 环境变量或者用户显式配的 apiKeyHelper 脚本),完全不去碰 OAuth 和 keychain。

剩下的几个常用入口:claude --ide 启动时自动探测当前打开的 IDE 并连上(前提是只有一个有效 IDE 实例,避免歧义);claude mcp serve 把当前进程变成 stdio MCP 服务器,claude mcp list 管理 MCP 配置。

claude server 启一个 HTTP 或 Unix socket 的 session server,支持 --port --host --unix --max-sessions --idle-timeout 等一整套参数:这个是给企业内部多用户共享一个 agent 进程的场景准备的。

剩下的认证(auth loginauth statusauth logout)、插件管理(plugin listplugin marketplace)、初始 token 配置(setup-token)、排错(doctor)、自动更新(update)、列出子 agent 定义(agents)都是各自独立的子命令。

跟 Codex 比起来,Claude Code 的取舍是”用户只装一个 npm 包”(分发简单),代价是 main.tsx 这个文件长达一万多行(维护成本高)。这两种取舍没有绝对优劣,更多取决于团队对”用户体验 vs 工程模块化”的优先级判断。

OpenClaw · 28 个懒加载子 CLI + 把频道做成可独立发布的 npm 包

Section titled “OpenClaw · 28 个懒加载子 CLI + 把频道做成可独立发布的 npm 包”

多频道平台需要把入口与 gateway 解耦。OpenClaw 的 sub-CLI 懒加载,频道以 plugin 发布,RPC 和 ACP 负责把外部调用带回同一运行时。

在本文固定的 commit 中,entries 数组注册了 28 个 sub-CLI。每个条目对应一个功能领域,主程序负责识别命令,再按需导入对应模块。

OpenClaw openclaw/src/cli/program/register.subclis.ts:44-310 28 个顶层 sub-CLI,从 acp、gateway 到 update、completion;每个条目延迟导入自己的注册模块
const entries: SubCliEntry[] = [
{ name: "acp", description: "Agent Control Protocol tools", hasSubcommands: true, register: ... },
{ name: "gateway", description: "Run, inspect, and query the WebSocket Gateway", hasSubcommands: true, register: ... },
{ name: "daemon", description: "Gateway service (legacy alias)", hasSubcommands: true, register: ... },
{ name: "logs", description: "Tail gateway file logs via RPC", hasSubcommands: false, register: ... },
{ name: "system", description: "System events, heartbeat, and presence", hasSubcommands: true, register: ... },
{ name: "models", description: "Discover, scan, and configure models", hasSubcommands: true, register: ... },
{ name: "approvals", description: "Manage exec approvals (gateway or node host)", hasSubcommands: true, register: ... },
{ name: "sandbox", description: "Manage sandbox containers for agent isolation", hasSubcommands: true, register: ... },
{ name: "tui", description: "Open a terminal UI connected to the Gateway", hasSubcommands: false, register: ... },
{ name: "cron", description: "Manage cron jobs via the Gateway scheduler", hasSubcommands: true, register: ... },
{ name: "channels", description: "Manage connected chat channels (Telegram, Discord, etc.)", hasSubcommands: true, register: ... },
{ name: "directory", description: "Lookup contact and group IDs (self, peers, groups) for supported chat channels", hasSubcommands: true, register: ... },
// ...
];

这种设计落到运行时形成了一个清晰的四层结构:

第一层是 gateway daemon:一个常驻的后台进程,持有所有 WebSocket 连接、全局状态、活跃 session。这是整个系统的「心脏」,用 openclaw gateway start 启动它。

第二层是 28 个 sub-CLIentries 先注册轻量 action,命中命令后再 import() 对应模块。源码能证明未选模块不会在注册阶段加载;它能省多少启动时间,要用同一构建、机器和缓存状态跑 benchmark。

第三层是独立的 TUI 应用openclaw tui):它跟 sub-CLI 是完全独立的 npm 包(在仓库里叫 ui-tui),通过 WebSocket 连到 gateway。这种独立性意味着 TUI 可以独立迭代、独立发版,甚至可以在不同机器上跑(TUI 在用户笔记本上,gateway 在公司服务器上)。

第四层是 ACP 服务器openclaw acp serve):ACP 是「Agent Control Protocol」的缩写,是一个标准化的 agent 通信协议(类似 MCP 但更聚焦于 agent 互操作),让外部 IDE 或者编排系统能通过标准协议跟 OpenClaw 的 agent 对话。

频道(Telegram、Discord、Slack 这些聊天平台)的接入方式更特别:它们不像 Hermes 那样内置在主仓库里,而是走「插件即 npm 包」的路线:

OpenClaw openclaw/src/channels/plugins/catalog.ts:1-58 频道作为 npm 包通过 catalog 注册,每个 channel 一个 `@openclaw/channel-<name>` 包
type ChannelPluginCatalogEntry = {
id: string;
meta: ChannelMeta;
install: {
npmSpec: string;
localPath?: string;
defaultChoice?: "npm" | "local";
};
};
const DEFAULT_CATALOG_PATHS = [
path.join(CONFIG_DIR, "mpm", "plugins.json"),
path.join(CONFIG_DIR, "mpm", "catalog.json"),
path.join(CONFIG_DIR, "plugins", "catalog.json"),
];

用户接入 Telegram 的流程是先跑 openclaw plugins add @openclaw/channel-telegram 装 Telegram 频道插件(本质上就是装一个独立的 npm 包),然后 openclaw channels add telegram 配置接入参数。这么做有几个重要的工程意义:第一,主仓库 size 不会爆炸(不需要包含所有平台的 SDK 依赖);

第二,生态可以贡献新通道(任何第三方都可以发布一个 @vendor/channel-myplatform 包);第三,每个频道插件可以独立版本管理(Telegram SDK 升级不影响 Discord 插件)。

Hermes · 一个 gateway 进程同时跑 17 个平台 adapter,把 HTTP API 也建模成”平台”

Section titled “Hermes · 一个 gateway 进程同时跑 17 个平台 adapter,把 HTTP API 也建模成”平台””

ChatOps 更在意平台适配的一致接口。Hermes 让 adapter 实现连接、发送、输入指示和会话查询等方法,HTTP 与 webhook 也走同一层。

Hermes 的核心抽象是一个叫 BasePlatformAdapter 的基类:它定义了 6 个所有平台都需要实现的方法,并且通过这 6 个方法的最小公共子集统一了 17 种千差万别的聊天平台。

Hermes hermes-agent/gateway/platforms/base.py:854-1055 BasePlatformAdapter:抽象出 connect / disconnect / send / send_typing / send_image / get_chat_info 等 6 个 abstract method,子类需要实现
class BasePlatformAdapter(ABC):
"""
Base class for platform adapters.
Subclasses implement platform-specific logic for:
- Connecting and authenticating
- Receiving messages
- Sending messages/responses
- Handling media
"""
def __init__(self, config: PlatformConfig, platform: Platform):
self.config = config
self.platform = platform
self._message_handler: Optional[MessageHandler] = None
self._running = False
# Track active message handlers per session for interrupt support
self._active_sessions: Dict[str, asyncio.Event] = {}
self._pending_messages: Dict[str, MessageEvent] = {}
# Background message-processing tasks spawned by handle_message().
self._background_tasks: set[asyncio.Task] = set()
# ...
@abstractmethod
async def connect(self) -> bool: ...
@abstractmethod
async def disconnect(self) -> None: ...
@abstractmethod
async def send(self, chat_id: str, content: str, ...) -> SendResult: ...

这 6 个抽象方法分别覆盖了「接进来、断开、发出去、显示输入指示、发图、查会话元信息」这几件每个聊天平台都需要支持的基本能力。子类需要实现这 6 个方法的具体逻辑:比如 TelegramAdapter 的 connect 是登录到 Telegram Bot API 加开 long polling,DiscordAdapter 的 connect 是连 Discord 的 gateway WebSocket,EmailAdapter 的 connect 是连 IMAP server。

基类还预先做了几件所有 adapter 都需要的事情:管理「活跃 session 的中断信号」(让用户能在 agent 中途打断生成)、管理「挂起的消息」(适配某些平台需要排队的场景)、管理「后台任务集合」(防止异步任务被 GC)。

17 个平台的实际注册在 _create_adapter 这个方法里一字铺开:

Hermes hermes-agent/gateway/run.py:2679-2817 GatewayRunner._create_adapter:根据 Platform 枚举创建对应 adapter,每个 platform 配 check_xxx_requirements() 探测依赖
def _create_adapter(self, platform: Platform, config: Any) -> Optional[BasePlatformAdapter]:
if platform == Platform.TELEGRAM:
from gateway.platforms.telegram import TelegramAdapter, check_telegram_requirements
if not check_telegram_requirements():
logger.warning("Telegram: python-telegram-bot not installed")
return None
return TelegramAdapter(config)
elif platform == Platform.DISCORD: ...
elif platform == Platform.WHATSAPP: ...
elif platform == Platform.SLACK: ...
elif platform == Platform.SIGNAL: ...
elif platform == Platform.HOMEASSISTANT: ...
elif platform == Platform.EMAIL: ...
elif platform == Platform.SMS: ...
elif platform == Platform.DINGTALK: ...
elif platform == Platform.FEISHU: ...
elif platform == Platform.WECOM_CALLBACK: ...
elif platform == Platform.WECOM: ...
elif platform == Platform.WEIXIN: ...
elif platform == Platform.MATTERMOST: ...
elif platform == Platform.MATRIX: ...
elif platform == Platform.API_SERVER: ...
elif platform == Platform.WEBHOOK: ...
elif platform == Platform.BLUEBUBBLES: ...
elif platform == Platform.QQBOT: ...
return None

这里有几个值得专门讲的设计细节:

第一个细节是依赖能力探测。每个 platform 在它的模块里都自带一个 check_<platform>_requirements() 函数(比如 check_telegram_requirementscheck_discord_requirements),专门用来检测这个 platform 需要的 Python SDK 是否装好。如果检测到没装,就只打一条警告日志然后跳过这个 adapter(既不报错也不抛异常)。这种「优雅降级」的设计让 Hermes 可以做到一件很灵活的事情:用户只装一个 pip install hermes-agent 主包就能拿到所有 17 个平台的代码,但只有那些用户配了凭证(通过 TELEGRAM_TOKEN、DISCORD_TOKEN 等环境变量)的平台才会启动 adapter。如果用户只想用 Telegram,那 discord.py、slack-bolt 这些 SDK 根本不会被装上。

第二个细节是 extra dependencies。如果用户需要某个平台,Python 的 packaging 系统提供了 hermes-agent[slack] 这种「extra」语法可以安装平台专属的依赖(slack-bolt 等)。这让「主包小加按需安装平台 SDK」成为可能。

第三个细节是把 HTTP API 和 Webhook 也建模成 platform:这是 Hermes 巧妙的一个设计。API_SERVER adapter 让 Hermes 暴露一个 HTTP API endpoint,外部系统可以通过 HTTP 调用让 agent 处理一条消息;WEBHOOK adapter 反过来让外部系统通过 webhook 主动推消息进来。这两种「非聊天平台」的入口完全可以走另一条独立的代码路径,但 Hermes 把它们也建模成 platform,复用了同一套 BasePlatformAdapter 抽象、同一套消息处理器、同一套 session 管理逻辑:结果是 API 调用和 webhook 都能享受到聊天平台那套的较全功能(活跃 session 中断、流式响应、长任务支持)。

除了 17 个 platform adapter,Hermes 还提供了几个面向开发者和工具链的入口:一个简单的 cli.py 直接跟 agent 对话(主要给开发和测试用)、一个 tui_gateway/ 包提供本地 TUI(连本地 gateway daemon)、一个 mcp_serve.py 把整个 Hermes 包装成一个 MCP server 让其他 agent 可以把它当工具用。

虽然四家在入口架构上的取舍差异巨大,但以下四个检查项在引用的源码中反复出现;它们是本文的工程归纳,不是所有入口系统都必须采用的固定集合。

第一件事是优先评估公开的 agent 协议。本次快照都暴露了 MCP 或 ACP 入口:Codex 是独立 binary,Claude Code 是子命令,Hermes 是 Python 模块,OpenClaw 是 ACP server。这说明互操作是一个现实需求,但不能仅凭四个项目就宣布某个协议已经成为普遍事实标准;选型仍要看调用方和版本兼容。

第二件事是按生命周期决定 CLI 和 daemon 是否分层。OpenClaw 通过常驻 gateway、Codex 通过 app-serverstdio-to-uds、Claude Code 通过 server 子命令展示了三种实现。长任务或多客户端共享状态时分层有价值;短命令、单进程工具则可能不需要 daemon,不能把它写成所有项目的终点。

第三件事是入口需要做能力探测。装了某个 channel、platform 不代表它能跑:SDK、凭证或外部服务都可能缺失。快照中的 Hermes、OpenClaw 和 Codex 都有不同形式的探测或声明;缺依赖时可以 warning 加跳过,但是否允许继续启动要由产品的失败策略决定。

第四件事是优先复用公开协议,并把私有扩展隔离在适配层。MCP、ACP、Telegram Bot API、Slack Bolt、Discord Gateway、IMAP 和 SMTP 都有公开规范或 SDK;这样能降低接入成本,但仍需记录版本、能力差异和供应方限制,不能从样本推断「没人造私有协议」。

四家多通道入口在通道数量 x 生态开放度上的相对位置
Codex multi-binary 偏左中;Claude Code 单 binary + subcommand 偏左下;OpenClaw 28 sub-CLI + plugins;Hermes 17 platform 内置主仓库。

四种典型场景:

  • 要做开发工具型 agent:参考 Codex / Claude Code 的 CLI + IDE + MCP 三件套。两家都有较全方案。
  • 要做 ChatOps 型 agent:参考 Hermes 的 BasePlatformAdapter + check_<platform>_requirements() pattern;17 个 platform 是这个快照的样本,不是覆盖目标。
  • 要做平台型 agent 框架:参考 OpenClaw 的 channels-plugins + ACP 设计,让生态做新通道。
  • 要做双栖 agent(开发 + ChatOps):参考 Hermes 的「HTTP API 也是 platform」+ Codex 的 stdio-to-uds 桥接,复用同一套 message handler。

选择:通道只翻译,不拥有状态

Section titled “选择:通道只翻译,不拥有状态”

这里不做星级评分。入口越多,越要先固定一条共享的运行状态和事件协议。

部署约束可先读代价与边界
终端、CI、IDE 需要不同生命周期Codex 的子命令、app-server 与 stdio-to-UDSbinary 与协议数量增加
想在一个 CLI 中维护交互和脚本入口Claude Code 的 commander 子命令与 —bare单包升级方便,但启动路径容易变重
需要让社区发布新频道并共享 gateway 状态OpenClaw 的懒加载 sub-CLI、RPC 和 channel plugin运行依赖常驻 gateway
主要服务 Telegram、Slack 等聊天平台Hermes 的 BasePlatformAdapter 与平台检查adapter 和平台测试矩阵持续增长

下面是从引用实现整理出的起步清单。先固定共享状态和调用契约,再按入口生命周期添加桥接、认证和升级策略。

复刻方案

最小可行

  • 先按调用方选入口:人类交互用 TUI 或 REPL,自动化用 -p / exec,外部工具链需要时再加 MCP server;不要为了凑齐模式同时维护没人使用的入口
  • 加 --bare 模式(参考 Claude Code):跳过所有「自动化」行为(如 auto-update、welcome message、analytics opt-in 等)给 CI、SDK 用。这些自动化在交互场景是友好但在 CI 是噪声
  • 认证入口独立子命令(参考 Claude Code 的 auth、Codex 的 login):login、status、logout 是独立子命令而非主 flag。这样 auth 流程可以独立测试、独立日志、独立 UX
  • doctor 子命令(参考 Claude Code 的 doctor):检查依赖、网络、配置、凭证。用户排错时第一步就跑 doctor,避免「卡在某个步骤但不知道哪一步出问题」的痛苦

进阶

  • multi-binary 拆分(参考 Codex):daemon(长跑服务)/ CLI(用户入口)/ mcp-server(MCP 协议端点)/ stdio-to-uds(桥)分别独立 binary,各自升级;不同部分升级速度不同,分 binary 能精细化
  • stdio-to-uds 桥(参考 Codex):IDE 子进程走 stdio,桥再通过 UDS 连接 daemon。它把进程生命周期分开,但吞吐、重连和升级行为仍要在目标 IDE 与 daemon 上验证
  • app-server 用 axum(参考 Codex):同一个 daemon 同时支持 HTTP1 + WS(不同 client 选协议);axum 是 Rust 生态最流行的 web server,性能 + 生态都成熟
  • sub-CLI lazy-load(参考 OpenClaw):每个 sub-CLI(auth / config / doctor / login 等)一个文件,只在用到时 import;用冷、热启动 benchmark 验证收益,不预写固定毫秒数
  • channels-plugins catalog(参考 OpenClaw):通道作为独立 npm 包通过 catalog 注册;这样新增通道(如新的 IM 平台)只需发 npm 包不用改主包,企业可以发自己的私有 channel 包
  • ACP 协议(参考 OpenClaw):让外部 IDE / orchestrator 用标准协议接入;不要发明私有协议(外部生态不会跟进),ACP 是 OpenClaw 推动的标准
  • BasePlatformAdapter(参考 Hermes):围绕 connect / disconnect / send / send_typing / send_image / get_chat_info 抽出 6 个 abstract method;新增平台还要补依赖、认证和兼容性测试
  • check_<platform>_requirements()(参考 Hermes):缺依赖时优雅降级 + 警告(如「telegram 库未装,telegram 通道禁用」),不 fatal;让用户能逐步开通道而非一次性装齐所有依赖
  • HTTP API / Webhook 也建模为 platform(参考 Hermes):复用同一套 message handler;HTTP 跟 Telegram 在 agent 视角都是「收到一条消息回一条消息」,统一抽象简化代码
  • extra dependencies(参考 Hermes):主包不带平台 SDK,hermes[slack] / hermes[discord] / hermes[telegram] 等 extra 才装对应 SDK;用户只用 Telegram 不应该被迫装 discord.py

一开始别做

  • 别把所有入口逻辑塞进 main 文件:按生命周期和职责拆 registrar、adapter 与 daemon;是否拆分看变更耦合和测试边界,不用固定行数判断
  • 别把所有 platform 强制依赖:用户只用 Telegram 不应该被迫装 discord.py / slack-sdk 等所有平台 SDK;用 extra 让用户选
  • 别让长任务占住 MCP 请求却没有进度和取消:使用 progress notification、任务句柄或分段请求,并对 stdio 缓冲与断线恢复做压力测试
  • 别在已有 MCP / ACP 能表达需求时再造私有协议:私有协议会把每个 IDE 的适配、版本兼容和文档都变成自己的维护成本
  • 别让 daemon 跟 CLI 紧耦合:daemon 升级不应该重启所有 CLI client(用户的 long-running session 会断),用 stdio-to-uds 桥解耦
四种入口拓扑并列对照
Codex 多 binary 各自独立;Claude Code 单 binary + commander 子命令;OpenClaw 28 sub-CLI + ACP + channels-plugins;Hermes 17 platform adapter 单进程。

把 4 种放一起,“用户怎么进来”的差异一眼可见:Codex 让每种调用方走专属 binary,Claude Code 让 commander 替你 dispatch,OpenClaw 让生态做新通道,Hermes 把 17 个平台塞同一个进程。

入口只是适配器,不能成为第二套 Agent 内核。核心状态、权限和生命周期只有一个真相源,通道负责认证、格式转换、流式反馈和能力裁剪。

下一步实验:在 IDE、聊天和 API 间操作同一个 Run,依次注入重复消息、乱序事件、取消后重连、客户端版本落后和通道掉线。验收只有一个运行实例、取消不重复、事件顺序可恢复、敏感状态不跨通道泄露,并记录每个入口的恢复延迟。

按需展开练习和十道复盘题
  1. 🟢 4 入口对比:用一句话总结 Codex / Claude Code / OpenClaw / Hermes 的”主入口”是什么。各自的非交互模式怎么进入?
  2. 🟠 实现 BasePlatformAdapter:写一个 Python BasePlatformAdapter 抽象类,包含 connect / disconnect / send / send_typing / get_chat_info 5 个 abstract method。实现 StdinAdapter(从 stdin 读消息)跟 WebhookAdapter(HTTP POST 接消息)两个子类。
  3. 🟠 lazy-load sub-CLI:用 commander.js 写一个 CLI,3 个子命令 logs / sandbox / models,每个子命令的实现在独立文件,只在用户跑这个子命令时才 import。
  4. 🔴 stdio-to-uds 桥:写一个 Node.js 程序,从 stdin 读 JSON-RPC 消息,转发到本地 UDS(/tmp/agent.sock);从 UDS 收到的响应再写回 stdout。验证:你的 IDE 启 node bridge.js 子进程,本地另启 agent-daemon 监听 UDS,两端能 RPC 通信。
Q1 · 概念:为什么 Codex 选 multi-binary,Claude Code 选 single-binary?

两个选择各有 trade-off,对应不同生态定位:

Codex multi-binary:每个入口(tui / exec / mcp-server / app-server / stdio-to-uds / app / cloud)是独立 Rust binary。好处:

  1. 独立升级:tui 改了 UI 不影响 mcp-server。VS Code 插件可以单独升 stdio-to-uds 而不动 tui。
  2. 依赖面可分开:每个 binary 只 link 自己需要的 crate;具体启动时间仍取决于构建模式、机器和初始化路径。
  3. 职责清晰:用户看一眼 codex-app-server 就知道是 daemon,看 codex 就知道是 TUI。
  4. 可分发到不同发布渠道:app 可以单独发 macOS App Store / Windows Store。

代价:用户记忆负担大(8 个 binary),打包跟分发复杂(需要 manifest 管多个 binary)。

Claude Code single-binary:用 commander.js 把所有入口塞 main.tsx,subcommand 路由。好处:

  1. npm install -g @anthropic-ai/claude-code 就是 all-in-one:用户不用区分 binary。
  2. 代码 / 类型 / 测试都在一个项目:refactor 跨子命令零成本。
  3. commander 自动 help / 子命令 dispatch:开发负担小。
  4. 跟 npm 生态对齐:bin field 一条命令,自动生成 PATH 入口。

代价:main.tsx 万行单文件;启动开销重(plugin / LSP / hook 都 load);改动一个子命令容易 break 其他。

选哪种?

  • agent 写 Rust + 重度多入口:Codex multi-binary 更适配这种发布与生命周期边界
  • agent 写 Node / Python + npm/pip 一键安装:Claude Code single-binary 更适配一键分发;是否采用仍取决于入口数量和升级策略
  • agent 写 Rust + 入口少:single binary + clap subcommand 也行(小 Codex)

追问:「混合方案?」可以。Codex 同时有独立 binary 和主 CLI 内的子命令分发;OpenClaw 则是「主 CLI + lazy-load sub-CLI module」。是否继续拆 binary,取决于生命周期和发布边界。

源码codex/codex-rs/cli/src/main.rs:105-182 + claude-code/src/main.tsx:971-1006

Q2 · 概念:什么时候值得接公开协议,什么时候需要私有扩展?

「公开协议 vs 私有协议」的成本算清楚:

私有协议成本

  • 每个 IDE / orchestrator 都要单独适配
  • 文档 / SDK / 版本兼容自己维护
  • 用户被锁定 → 切换难 → 生态封闭

公开协议(MCP / ACP)的潜在收益

  • 对兼容客户端减少重复适配
  • 复用现成文档、SDK 和版本协商机制
  • 把私有扩展限制在能力缺口,而不是重写整个传输层

本次快照里的入口:

  • MCP(Model Context Protocol):Codex、Claude Code 和 Hermes 的引用路径提供 MCP server 入口,但具体 client、能力和版本需要逐一核对。
  • ACP(Agent Client Protocol):OpenClaw 的引用路径提供 ACP server;它与 MCP 解决的调用关系不同,不能把两者当成一个统一协议。

四个入口并不等于“都采用同一协议”:

  • Codex mcp-server binary
  • Claude Code claude mcp serve
  • OpenClaw openclaw acp 子命令
  • Hermes mcp_serve.py

实际效果要验证: 只有当客户端实现相同协议版本、transport 和所需能力时,接入才可能减少适配;本文没有在 Cursor 中同时挂载三套 server 的实测。

追问:「公开协议要不要等成熟再用?」先看调用方、版本稳定性和缺失能力。早期接入能换来生态兼容,也会承担 breaking change 和兼容测试;把私有字段隔离在 adapter,能降低后续迁移成本。

源码codex/codex-rs/mcp-server/openclaw/src/acp/server.tshermes-agent/mcp_serve.py

Q3 · 架构:stdio-to-uds 桥设计为什么这么重要?

IDE 集成有一个根本张力:

IDE 端约束

  • 启子进程走 stdio(VS Code Extension API、JetBrains Plugin SDK 都这么设计)
  • 进程退出时子进程需要清理(防止僵尸进程)

Agent 端约束

  • 长任务要在 daemon 里跑(用户关 IDE 不应该 kill agent task)
  • 多 IDE / 多 client 共享同一 agent state

冲突: IDE 子进程模型跟「daemon 长跑」天然矛盾。

stdio-to-uds 桥解决方案:

IDE 启 stdio-to-uds 子进程 (stdio 接 IDE)
stdio-to-uds 内部连 UDS (Unix domain socket)
agent-daemon 监听 UDS(独立长跑进程)

桥本身只负责转发和重连;用户关 IDE 时 stdio-to-uds 退出,但 daemon 是否继续、session state / cache 是否保留,取决于 daemon 生命周期策略。内存占用需要在目标构建上测量,源码没有给出可迁移的 <5MB 基准。

桥的具体职责:

  1. 转发 stdio JSON-RPC 消息到 UDS
  2. 翻译 stdio vs UDS 的协议差异(如果有)
  3. 重连:IDE 重启后桥重连 UDS,daemon 不感知
  4. 复用:多个 IDE 实例同时连同一 daemon

Windows 怎么办? UDS 在 Windows 10+ 也支持。或者用 named pipe (\\.\pipe\agent) 等价方案。

追问:「不用桥,IDE 直接连 UDS 行不行?」取决于目标 IDE extension API 和部署方式。只有当客户端只能启动 stdio 子进程、而服务必须独立常驻时,桥才是必要层;否则可以直接使用 socket、HTTP 或 WebSocket。

Codex 是单一显式做这件事的 agentstdio-to-uds 是独立 crate。其他三家要做 IDE 集成时也会面对同样问题,但还没单独抽出来。

源码codex/codex-rs/stdio-to-uds/

Q4 · 概念:Hermes 把 HTTP API 跟 Webhook 也建模为 platform 的好处?

「platform adapter」原本是为聊天平台抽象的(Telegram / Discord / Slack)。Hermes 把 HTTP API 跟 inbound webhook 也塞进同一抽象。好处:

1. 复用 message handler 逻辑

agent 的 message handler 不关心消息从哪来。Telegram 用户发消息跟 HTTP API 调用,到 agent 这一层都是 MessageEvent { content, chat_id, user_id }。同一套 connect / send / disconnect 抽象,同一套响应回流逻辑。

2. 统一 multi-channel 行为

「用户在 Telegram 提问 → agent 在 Telegram 回复」跟「外部系统 POST 到 webhook → agent POST 回 webhook」逻辑一样。如果 HTTP / webhook 不是 platform,要写两套响应路由。

3. session 管理统一

每个 platform adapter 自管 _active_sessions: Dict[str, asyncio.Event]。HTTP / webhook 也复用这个 session 概念:HTTP 调用方传 session_id,agent 就把这个 HTTP request 关联到 session。

坏处:

  1. HTTP / webhook 没有「typing indicator」这种平台特性。BasePlatformAdapter 强行抽象 6 method,HTTP adapter 只能 no-op 实现 send_typing()
  2. 认证模式跟聊天 platform 完全不同:Telegram 用 token,HTTP 用 API key / JWT。抽象到一起会增加耦合。
  3. 协议假设漂移:HTTP 是请求-响应,Telegram 是事件驱动。两个塞同一抽象需要 adapter 自己 bridge。

Hermes 的做法适合什么场景?

Hermes 定位是「ChatOps 优先」。在 HTTP API 与 Webhook 只是次要入口时,复用 platform adapter 可以减少额外维护面;如果 HTTP API 成为主入口,则应评估反过来的设计(HTTP-first,聊天 platform 作为 adapter)。

追问:「自己实现这种 unified adapter 要注意?」抽象层做最小(5-6 method),让具体 adapter 决定细节。Hermes 抽象 send_typing 但允许 no-op,这是 escape hatch。

源码hermes-agent/gateway/platforms/base.py:854-1060

Q5 · 工程:lazy-load sub-CLI 在 OpenClaw 怎么实现?

本文固定的 OpenClaw commit 注册了 28 个 sub-CLI(acp / gateway / tui / channels / plugins / sandbox / …)。全量 import 会扩大启动路径;这里没有提供可复现的耗时结果,因此只讨论 lazy-load 的代码结构。

实现思路:

register.subclis.ts
program
.command('channels')
.description('manage channels')
.action(async () => {
const { run } = await import('./subclis/channels.js')
return run()
})
program
.command('sandbox')
.description('manage sandboxes')
.action(async () => {
const { run } = await import('./subclis/sandbox.js')
return run()
})

关键点:

  1. action 是 async:commander 支持 async action,import 完才 run。
  2. dynamic import import('./...'):只在 user 跑这个子命令时才 load 那个文件。
  3. 每个子命令对应一个独立文件:实现拆分,每个文件只 import 自己需要的依赖。

怎样测启动收益:

在同一 commit 和 production build 下,分别开启、关闭 OPENCLAW_DISABLE_LAZY_SUBCOMMANDS,对 openclaw --help 与一个真实子命令各跑多次,报告冷启动、热启动和分位数。没有这组数据时,只能说 lazy-load 缩短了 import 路径,不能写成 10 倍提速。

坏处:

  1. 类型检查变复杂:dynamic import 的类型推断需要 TypeScript 4.5+ 才好用
  2. 打包工具配置:webpack / esbuild / Vite 需要正确处理 dynamic import(默认应该 OK)
  3. 测试稍微麻烦:每个 sub-CLI 单独测试要 mock 文件系统
  4. 错误处理:sub-CLI 文件被删时启动失败,需要明确错误信息

追问:「Python 怎么做?」importlib.import_module(name) 在 action 函数里。Click + lazy-loading 也支持。注意 Python 启动开销主要来自 import,所以效果更明显。

Hermes 没做 lazy-load:因为 platform adapter 都内置,启动时全 load。但每个 adapter 有 check_<platform>_requirements(),只有依赖装了的 adapter 才实际 instantiate。是另一种 lazy 策略。

源码openclaw/src/cli/program/register.subclis.ts

Q6 · 实战:你给自己的 agent 加多通道入口,从 0 到生产怎么走?

core CLI → MCP → IDE 集成 → 聊天平台 四个 entry scope 推进;每层先用协议、生命周期和依赖 fixture 验收。

阶段 1 · core CLI

main.py
import click
@click.group()
def cli(): pass
@cli.command()
def chat():
"""Interactive REPL"""
run_repl()
@cli.command()
@click.option('-p', '--prompt')
def exec(prompt: str):
"""One-shot script mode"""
print(run_once(prompt))
@cli.command()
def doctor():
"""Diagnose installation"""
check_dependencies()
if __name__ == "__main__":
cli()

参考 Codex codex + codex exec 思路。

验收门槛:交互、one-shot 和 doctor fixture 都能返回结构化结果,依赖缺失时给出可行动错误。

阶段 2 · MCP server

前置:core CLI 的状态和退出码契约已经稳定。

mcp_serve.py
from mcp.server import Server
server = Server("my-agent")
@server.list_tools()
async def list_tools():
return [Tool(name="ask", description="Ask the agent")]
@server.call_tool()
async def call_tool(name, args):
if name == "ask":
return run_once(args["prompt"])
if __name__ == "__main__":
server.run_stdio()

参考 Hermes mcp_serve.py

验收门槛:list/call、参数校验、版本协商和错误映射在独立 MCP client fixture 中通过。

阶段 3 · IDE 集成

前置:MCP protocol trace 已经可回放,且明确 stdio/daemon 的生命周期边界。

走 MCP 路线:Cursor / VS Code / Cline 都已支持 MCP server。用户在 IDE 配 mcp_servers.json

{
"my-agent": {
"command": "python",
"args": ["-m", "my_agent.mcp_serve"]
}
}

IDE 启动子进程,stdio 跟 agent 通信。所有 IDE 共用同一 protocol。

不要自己写私有协议。

验收门槛:IDE 关闭/重启、daemon 重连和 session 恢复可重放,协议错误不会卡住客户端。

阶段 4 · 聊天平台(按需)

前置:有明确的平台调用方,且 adapter 生命周期、认证和脱敏规则已定义。

参考 Hermes BasePlatformAdapter

class BasePlatformAdapter:
async def connect(self): ...
async def disconnect(self): ...
async def send(self, chat_id, content): ...
async def handle_message(self, event): ...
class TelegramAdapter(BasePlatformAdapter):
async def connect(self):
self.app = Application.builder().token(TOKEN).build()
await self.app.start()
# ...

check_telegram_requirements() 检查 python-telegram-bot 装没装,缺了优雅降级。

验收门槛:每个平台的 connect、send、receive、断线恢复和依赖诊断都有 fixture,缺依赖不会误报已连接。

关键经验:

  1. 先做 CLI,don’t gold-plate:先让一个用户能用
  2. 协议入口复用 MCP:不要自己造私有协议
  3. CLI + MCP 通过 fixture 后再加 channel:先把核心路径跑稳
  4. 按真实需求逐个加 Telegram / Discord 等:每个 channel 是一个可回滚的变更

追问:「daemon vs 进程模型?」第一阶段不需要 daemon,每次启动都 fresh state。需要 session 跨进程时(IDE 关了又开)才上 daemon + UDS。先简单后复杂。

Q7 · 概念--bare 模式跟 -p(脚本模式)有什么本质区别?

两个都是「非交互模式」,但抽象层不同:

-p / --print / exec

  • 输入一个 prompt,输出 final answer
  • 中间过程(tool call / thinking / progress)默认不显示
  • 适合:bash pipe (echo prompt | claude -p)、shell script、简单自动化

--bare

  • 跳过所有 “smart” 行为:plugin、LSP、hook、auto-detect IDE、telemetry
  • 但仍然可以交互,仍然有较全 tool support
  • 适合:CI 环境、SDK 调用、需要可预测行为的场景

为什么需要 --bare

CI 跑 claude 时遇到的真实问题:

  • plugin 自动 load → CI 失败因为 plugin 缺依赖
  • LSP 启动 → CI VM 不支持 → 启动 hang
  • telemetry 后台 → CI VM 出网络问题
  • auto-detect IDE → CI 是 docker container,detect 出错

--bare 一刀切关掉所有这些。CI 拿到的是「最小可工作 agent」。

vs -p

  • --bare -p prompt 是组合:CI 环境 + script 模式
  • --bare 单独用:CI 环境 + 可能交互(少见但有)
  • -p 单独用:dev 机 script 模式(带 plugin / LSP / hook)

Claude Code 的注释(注释里直接给的语义):

—bare: Skip all “automation” behavior. Useful for CI / SDK.

追问:「这种 flag 应当默认开还是默认关?」交互入口默认保留 plugin、LSP 和 hook,CI / SDK 显式开启 --bare。这是按入口语义做的默认值,不依赖未经统计的使用时长比例。

类似的设计:

  • npm 的 --ci flag
  • pip 的 --no-cache-dir
  • git 的 -c core.pager=cat

都是「禁掉某些智能行为,让结果可预测」的开关。

源码claude-code/src/main.tsx:971-1006(参数注册)+ main.tsx 各处 if (settings.bare) skip ...

Q8 · 概念:channels-plugins catalog 让生态贡献 channel 的好处和代价?

OpenClaw 把 channel 做成 npm package + catalog 注册。例子:

channels-plugins/catalog.ts
export const CHANNELS: Record<string, ChannelInfo> = {
telegram: {
package: '@openclaw/channel-telegram',
description: 'Telegram bot channel',
enabledBy: 'TELEGRAM_BOT_TOKEN',
},
discord: {
package: '@openclaw/channel-discord',
description: 'Discord bot channel',
enabledBy: 'DISCORD_BOT_TOKEN',
},
// ... 第三方贡献的也可以注册
}

好处:

  1. 生态可贡献:任何人写一个 @my-org/openclaw-channel-xxx npm 包,PR 加进 catalog 就能被其他用户用。OpenClaw 自己不需要维护这些 channel。
  2. 按需安装:用户 npm install @openclaw/channel-telegram 才装 Telegram 依赖。其他 channel 不会被强制依赖。
  3. 版本独立:channel 升级 SDK 时不需要 OpenClaw 主版本升。
  4. 关注点分离:channel 维护者关心自己 channel,OpenClaw 关心核心。

代价:

  1. 版本兼容矩阵:channel package version × OpenClaw version。某些组合可能不兼容,需要 catalog 注明兼容范围。
  2. 质量不齐:第三方 channel 可能 bug / 没维护 / 安全问题。Catalog 需要审核机制(OpenClaw 怎么背书 @my-org/...)。
  3. discovery 难:用户怎么知道有哪些 channel?需要 catalog UI + 文档。
  4. breaking change 协调:OpenClaw 改 ChannelAdapter API 时,所有 third-party channel 都要 follow。

vs Hermes 方案:

Hermes 把 17 platform 全内置主仓库。好处是兼容性修复可以集中落地,用户也不必寻找插件;代价是主仓库和平台测试矩阵持续增长,外部贡献更难独立发布。

两种方案的适用场景:

  • OpenClaw 模式:你期待长期有几十个 channel,且大多由生态维护
  • Hermes 模式:你预计会维护一组固定 channel,并愿意承担适配器与兼容性测试

追问:「能不能两条腿走?」可以。Hermes 主仓库带 17 个核心 platform,同时提供 plugin API 让第三方写额外 platform。OpenClaw 现在的 channels-plugins 实际也是「官方维护核心 + 留外部贡献空间」。这是混合模式。

源码openclaw/src/channels/plugins/catalog.ts

Q9 · 工程check_<platform>_requirements() 优雅降级方案的实现细节?

Hermes 的 17 platform 每个都有 check function:

def check_telegram_requirements() -> bool:
"""Check if Telegram dependencies are installed."""
try:
import telegram # python-telegram-bot
return True
except ImportError:
return False
def check_discord_requirements() -> bool:
try:
import discord
return True
except ImportError:
return False
# ...

_create_adapter 里使用:

def _create_adapter(self, platform, config):
if platform == Platform.TELEGRAM:
if not check_telegram_requirements():
logger.warning("Telegram: python-telegram-bot not installed")
return None
return TelegramAdapter(config)
# ...

为什么不直接 try: import telegram

  1. 报错信息更清晰logger.warning 明确告诉用户「依赖缺了」,而不是堆栈跟踪
  2. 可在配置阶段提前检测:不用等运行时第一次 import 才 fail
  3. 跟 platform 注册解耦:check function 是 module-level,可被多处调用

Pure ImportError vs check function 对比:

# 反模式:直接 import
def _create_telegram():
import telegram # 在第一次调用时才 ImportError
return TelegramAdapter(...)
# 正确模式:先 check
if check_telegram_requirements():
return TelegramAdapter(...)
else:
logger.warning("Telegram dependencies missing")
return None

进阶:optional dependencies in setup.cfg / pyproject.toml

[project.optional-dependencies]
telegram = ["python-telegram-bot>=20.0"]
discord = ["discord.py>=2.0"]
slack = ["slack-bolt>=1.0"]
all = ["python-telegram-bot", "discord.py", "slack-bolt", ...]

用户用 pip install hermes[telegram]pip install hermes[all]。主包不带平台 SDK,extras 才装。

追问:「为什么不让用户需要装所有 SDK?」用户可能只用 Telegram,没必要被迫装未使用的平台 SDK;具体依赖体积随版本和平台变化。pip extras 让用户按需选。

类似的设计: TensorFlow、PyTorch 等项目也提供可选依赖;是否采用「主包 + extras」要看安装体验、发布矩阵和运行时探测。

源码hermes-agent/gateway/run.py:2663-2820 + 每个 platform 文件的 check_xxx_requirements

Q10 · 开放:按入口生命周期设计一个共享状态框架,并给出兼容性测试。

5 层架构:

Layer 1 · 入口路由(需要)

// CLI dispatch
program
.command('chat').action(runRepl)
.command('exec').action(runScript)
.command('mcp serve').action(runMcpServer)
.command('server').action(runHttpServer)
.command('auth').action(runAuth)
.command('doctor').action(runDoctor);

参考 Claude Code commander dispatch。

Layer 2 · 协议 server 入口(需要)

# MCP server (Codex/Claude/Hermes)
from mcp.server import Server
server = Server("agent")
@server.call_tool()
async def call_tool(name, args): ...
server.run_stdio()
# ACP server (OpenClaw)
from agentclientprotocol import AcpServer
acp_server = AcpServer(handler)
acp_server.serve()

根据真实调用方选择一个已验证的协议入口;上面的 MCP 与 ACP 只是两种不同示例。

Layer 3 · 平台 adapter 抽象(可选,看场景)

class BasePlatformAdapter(ABC):
@abstractmethod
async def connect(self): ...
@abstractmethod
async def disconnect(self): ...
@abstractmethod
async def send(self, chat_id, content): ...
@abstractmethod
async def handle_message(self, event): ...
def check_platform_requirements(name: str) -> bool:
try:
import_platform_sdk(name)
return True
except ImportError:
return False

参考 Hermes BasePlatformAdapter + check_xxx_requirements

Layer 4 · daemon ↔ client 桥(可选,IDE 集成需要)

// stdio-to-uds bridge
async fn bridge(stdin: Stdin, stdout: Stdout) {
let uds = UnixStream::connect("/tmp/agent.sock").await?;
// 双向转发 JSON-RPC
tokio::join!(
forward_stdin_to_uds(stdin, uds.clone()),
forward_uds_to_stdout(uds, stdout),
);
}

参考 Codex stdio-to-uds crate。

Layer 5 · 入口探测 / doctor(需要)

def doctor():
"""Diagnose installation."""
checks = [
("Python version", check_python_version),
("MCP SDK", check_mcp_sdk),
("Network", check_network),
("API keys", check_api_keys),
]
for name, fn in checks:
ok, msg = fn()
print(f"[{'OK' if ok else 'FAIL'}] {name}: {msg}")

参考 Claude Code doctor 子命令。

贡献矩阵:

  • Codex 贡献:multi-binary 拆分 + stdio-to-uds 桥 + 公开协议 entry
  • Claude Code 贡献:commander.js 子命令 + --bare 模式 + doctor
  • OpenClaw 贡献:sub-CLI lazy-load + channels-plugins catalog + ACP
  • Hermes 贡献:BasePlatformAdapter + check_xxx_requirements + 17 platform 实现

按入口 scope 评估

  • Layer 1-2 · core CLI 与自动化入口:先固定共享状态、退出码和诊断契约。验收:交互与 one-shot fixture 都能启动、返回结构化结果,并在依赖缺失时给出可行动错误。
  • Layer 3 · 协议入口:只有出现明确的外部调用方才加入 MCP/ACP。前置:核心 API 已稳定;验收:list/call、版本协商和错误映射在独立客户端 fixture 中通过。
  • Layer 4 · IDE 桥接:仅在需要双向流、重连或跨进程 session 时加入。前置:协议入口已有端到端 trace;验收:IDE 关闭/重启、daemon 重连和状态恢复可重放。
  • Layer 5 · 聊天平台:按真实平台需求逐个加入,而不是一次铺开。前置:adapter 生命周期、认证和脱敏契约明确;验收:每个平台的 connect、send、receive、断线恢复与依赖诊断都有 fixture。

关键决策:

  1. 先交付一个人工入口和一个自动化入口:MCP 只在外部工具链成为明确调用方时进入首版
  2. 聊天 platform 不要急着做:等用户实际要求再加
  3. 不要造私有协议:MCP / ACP 已经够好
  4. doctor 早做:用户排错入口
  5. lazy-load 早做:先定义目标机器和启动 SLO,再用冷、热启动 benchmark 验证

追问:「微服务化是不是更好?」代码行数和团队规模都不能单独回答。只有当入口需要独立生命周期、权限或发布节奏时才拆进程;否则 single binary + subcommand 通常更容易起步。

源码组合codex/codex-rs/cli/ + codex/codex-rs/stdio-to-uds/ + claude-code/src/main.tsx + openclaw/src/cli/program/register.subclis.ts + hermes-agent/gateway/