14 · 多入口如何共享一条状态
让终端、IDE、协议和聊天入口共享一条可追踪、可升级的 Agent 状态
本章任务
要回答的问题
同一任务从终端、IDE、API 和聊天入口进入时,怎样共享身份、状态、取消和事件流?
读完你能
- 把入口适配与核心 Loop、Session 状态解耦
- 统一 run、thread、user 与 channel 身份
- 定义流式输出、取消、重连和版本升级语义
- 适合现在读
- 正在做 Agent 服务端、IDE 集成、多平台助手或协议适配的工程师
- 先修知识
- 理解 Session、事件流和入口鉴权
- 实践产物
- 一份入口适配器契约和多通道路由表
- 证据边界
- 共享状态不等于共享所有界面数据;隐私、能力和节奏仍应按通道裁剪
入口不同,核心状态不能分叉
Section titled “入口不同,核心状态不能分叉”场景:用户在 IDE 启动一次重构,随后从 Slack 补充约束,又通过 API 点取消;网络重连后 IDE 重发最后一条消息。若每个入口维护自己的“当前状态”,系统可能同时存在两个 Run、一次失效取消和一条重复输入。
通过标准:thread、run、user、channel 身份分开建模;所有入口写入同一有序事件流;取消按 run_id 幂等;重连从游标继续而不是重放副作用;每个通道只渲染适合自己的状态视图。
先固定一条共享状态
Section titled “先固定一条共享状态”四家在多通道入口上的覆盖:
| 维度 | Codex | Claude Code | OpenClaw | Hermes |
|---|---|---|---|---|
| 主交互形态 | TUI 默认加 exec 子命令脚本化 | REPL 默认加 -p、--print 脚本化 | gateway daemon 长跑加 sub-CLI client | gateway 单进程加 17 platform adapter |
| IDE 集成 | codex stdio-to-uds(VSCode、Cursor 走 UDS)加 codex mcp-server(stdio MCP) | --ide flag 自动 detect 加 IDE MCP。mcp serve 子命令开 MCP server | ACP server(@agentclientprotocol/sdk)加 gateway WebSocket | 不主打 IDE。走 Webhook、API server、Mattermost、Matrix 等 ChatOps |
| MCP、远程协议 | codex mcp-server (stdio) 加 codex app-server (axum HTTP+WS) 加 remote-control | mcp serve 子命令(stdio MCP)加 server 子命令(HTTP、Unix socket session server) | ACP server(standardized agent control protocol)加 WebSocket gateway | mcp_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 加 ratatui | main.tsx 里 ink 加 ratatui-like 渲染 | tui-cli 子命令加 ui-tui packages(独立 TUI 应用连 gateway) | tui_gateway 包,连本地 gateway daemon |
源码证据:入口如何回到核心
Section titled “源码证据:入口如何回到核心”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 login、auth status、auth logout)、插件管理(plugin list、plugin 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-CLI:entries 先注册轻量 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_requirements、check_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 可以把它当工具用。
通道实现共享的底线
Section titled “通道实现共享的底线”虽然四家在入口架构上的取舍差异巨大,但以下四个检查项在引用的源码中反复出现;它们是本文的工程归纳,不是所有入口系统都必须采用的固定集合。
第一件事是优先评估公开的 agent 协议。本次快照都暴露了 MCP 或 ACP 入口:Codex 是独立 binary,Claude Code 是子命令,Hermes 是 Python 模块,OpenClaw 是 ACP server。这说明互操作是一个现实需求,但不能仅凭四个项目就宣布某个协议已经成为普遍事实标准;选型仍要看调用方和版本兼容。
第二件事是按生命周期决定 CLI 和 daemon 是否分层。OpenClaw 通过常驻 gateway、Codex 通过 app-server 加 stdio-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;这样能降低接入成本,但仍需记录版本、能力差异和供应方限制,不能从样本推断「没人造私有协议」。
按部署形态选择入口
Section titled “按部署形态选择入口”四种典型场景:
- 要做开发工具型 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-UDS | binary 与协议数量增加 |
| 想在一个 CLI 中维护交互和脚本入口 | Claude Code 的 commander 子命令与 —bare | 单包升级方便,但启动路径容易变重 |
| 需要让社区发布新频道并共享 gateway 状态 | OpenClaw 的懒加载 sub-CLI、RPC 和 channel plugin | 运行依赖常驻 gateway |
| 主要服务 Telegram、Slack 等聊天平台 | Hermes 的 BasePlatformAdapter 与平台检查 | adapter 和平台测试矩阵持续增长 |
从一个入口与一个核心开始
Section titled “从一个入口与一个核心开始”下面是从引用实现整理出的起步清单。先固定共享状态和调用契约,再按入口生命周期添加桥接、认证和升级策略。
复刻方案
最小可行
- 先按调用方选入口:人类交互用 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 桥解耦
把入口差异放回部署场景
Section titled “把入口差异放回部署场景”把 4 种放一起,“用户怎么进来”的差异一眼可见:Codex 让每种调用方走专属 binary,Claude Code 让 commander 替你 dispatch,OpenClaw 让生态做新通道,Hermes 把 17 个平台塞同一个进程。
入口适配器怎样回到同一核心
Section titled “入口适配器怎样回到同一核心”本章带走什么与下一步实验
Section titled “本章带走什么与下一步实验”入口只是适配器,不能成为第二套 Agent 内核。核心状态、权限和生命周期只有一个真相源,通道负责认证、格式转换、流式反馈和能力裁剪。
下一步实验:在 IDE、聊天和 API 间操作同一个 Run,依次注入重复消息、乱序事件、取消后重连、客户端版本落后和通道掉线。验收只有一个运行实例、取消不重复、事件顺序可恢复、敏感状态不跨通道泄露,并记录每个入口的恢复延迟。
附录:练习与复盘
Section titled “附录:练习与复盘”按需展开练习和十道复盘题
- 🟢 4 入口对比:用一句话总结 Codex / Claude Code / OpenClaw / Hermes 的”主入口”是什么。各自的非交互模式怎么进入?
- 🟠 实现 BasePlatformAdapter:写一个 Python
BasePlatformAdapter抽象类,包含connect / disconnect / send / send_typing / get_chat_info5 个 abstract method。实现StdinAdapter(从 stdin 读消息)跟WebhookAdapter(HTTP POST 接消息)两个子类。 - 🟠 lazy-load sub-CLI:用 commander.js 写一个 CLI,3 个子命令
logs / sandbox / models,每个子命令的实现在独立文件,只在用户跑这个子命令时才 import。 - 🔴 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。好处:
- 独立升级:tui 改了 UI 不影响 mcp-server。VS Code 插件可以单独升 stdio-to-uds 而不动 tui。
- 依赖面可分开:每个 binary 只 link 自己需要的 crate;具体启动时间仍取决于构建模式、机器和初始化路径。
- 职责清晰:用户看一眼
codex-app-server就知道是 daemon,看codex就知道是 TUI。 - 可分发到不同发布渠道:app 可以单独发 macOS App Store / Windows Store。
代价:用户记忆负担大(8 个 binary),打包跟分发复杂(需要 manifest 管多个 binary)。
Claude Code single-binary:用 commander.js 把所有入口塞 main.tsx,subcommand 路由。好处:
npm install -g @anthropic-ai/claude-code就是 all-in-one:用户不用区分 binary。- 代码 / 类型 / 测试都在一个项目:refactor 跨子命令零成本。
- commander 自动 help / 子命令 dispatch:开发负担小。
- 跟 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-serverbinary - 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.ts、hermes-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 基准。
桥的具体职责:
- 转发 stdio JSON-RPC 消息到 UDS
- 翻译 stdio vs UDS 的协议差异(如果有)
- 重连:IDE 重启后桥重连 UDS,daemon 不感知
- 复用:多个 IDE 实例同时连同一 daemon
Windows 怎么办? UDS 在 Windows 10+ 也支持。或者用 named pipe (\\.\pipe\agent) 等价方案。
追问:「不用桥,IDE 直接连 UDS 行不行?」取决于目标 IDE extension API 和部署方式。只有当客户端只能启动 stdio 子进程、而服务必须独立常驻时,桥才是必要层;否则可以直接使用 socket、HTTP 或 WebSocket。
Codex 是单一显式做这件事的 agent:stdio-to-uds 是独立 crate。其他三家要做 IDE 集成时也会面对同样问题,但还没单独抽出来。
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。
坏处:
- HTTP / webhook 没有「typing indicator」这种平台特性。BasePlatformAdapter 强行抽象 6 method,HTTP adapter 只能 no-op 实现
send_typing()。 - 认证模式跟聊天 platform 完全不同:Telegram 用 token,HTTP 用 API key / JWT。抽象到一起会增加耦合。
- 协议假设漂移: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。
Q5 · 工程:lazy-load sub-CLI 在 OpenClaw 怎么实现?
本文固定的 OpenClaw commit 注册了 28 个 sub-CLI(acp / gateway / tui / channels / plugins / sandbox / …)。全量 import 会扩大启动路径;这里没有提供可复现的耗时结果,因此只讨论 lazy-load 的代码结构。
实现思路:
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() })关键点:
action是 async:commander 支持 async action,import 完才 run。- dynamic import
import('./...'):只在 user 跑这个子命令时才 load 那个文件。 - 每个子命令对应一个独立文件:实现拆分,每个文件只 import 自己需要的依赖。
怎样测启动收益:
在同一 commit 和 production build 下,分别开启、关闭 OPENCLAW_DISABLE_LAZY_SUBCOMMANDS,对 openclaw --help 与一个真实子命令各跑多次,报告冷启动、热启动和分位数。没有这组数据时,只能说 lazy-load 缩短了 import 路径,不能写成 10 倍提速。
坏处:
- 类型检查变复杂:dynamic import 的类型推断需要 TypeScript 4.5+ 才好用
- 打包工具配置:webpack / esbuild / Vite 需要正确处理 dynamic import(默认应该 OK)
- 测试稍微麻烦:每个 sub-CLI 单独测试要 mock 文件系统
- 错误处理: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 策略。
Q6 · 实战:你给自己的 agent 加多通道入口,从 0 到生产怎么走?
按 core CLI → MCP → IDE 集成 → 聊天平台 四个 entry scope 推进;每层先用协议、生命周期和依赖 fixture 验收。
阶段 1 · core CLI
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 的状态和退出码契约已经稳定。
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,缺依赖不会误报已连接。
关键经验:
- 先做 CLI,don’t gold-plate:先让一个用户能用
- 协议入口复用 MCP:不要自己造私有协议
- CLI + MCP 通过 fixture 后再加 channel:先把核心路径跑稳
- 按真实需求逐个加 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 的
--ciflag - 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 注册。例子:
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', }, // ... 第三方贡献的也可以注册}好处:
- 生态可贡献:任何人写一个
@my-org/openclaw-channel-xxxnpm 包,PR 加进 catalog 就能被其他用户用。OpenClaw 自己不需要维护这些 channel。 - 按需安装:用户
npm install @openclaw/channel-telegram才装 Telegram 依赖。其他 channel 不会被强制依赖。 - 版本独立:channel 升级 SDK 时不需要 OpenClaw 主版本升。
- 关注点分离:channel 维护者关心自己 channel,OpenClaw 关心核心。
代价:
- 版本兼容矩阵:channel package version × OpenClaw version。某些组合可能不兼容,需要 catalog 注明兼容范围。
- 质量不齐:第三方 channel 可能 bug / 没维护 / 安全问题。Catalog 需要审核机制(OpenClaw 怎么背书
@my-org/...)。 - discovery 难:用户怎么知道有哪些 channel?需要 catalog UI + 文档。
- 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 实际也是「官方维护核心 + 留外部贡献空间」。这是混合模式。
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?
- 报错信息更清晰:
logger.warning明确告诉用户「依赖缺了」,而不是堆栈跟踪 - 可在配置阶段提前检测:不用等运行时第一次 import 才 fail
- 跟 platform 注册解耦:check function 是 module-level,可被多处调用
Pure ImportError vs check function 对比:
# 反模式:直接 importdef _create_telegram(): import telegram # 在第一次调用时才 ImportError return TelegramAdapter(...)
# 正确模式:先 checkif 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 dispatchprogram .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 Serverserver = Server("agent")@server.call_tool()async def call_tool(name, args): ...server.run_stdio()
# ACP server (OpenClaw)from agentclientprotocol import AcpServeracp_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 bridgeasync 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。
关键决策:
- 先交付一个人工入口和一个自动化入口:MCP 只在外部工具链成为明确调用方时进入首版
- 聊天 platform 不要急着做:等用户实际要求再加
- 不要造私有协议:MCP / ACP 已经够好
- doctor 早做:用户排错入口
- 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/。