OpenClaw 源码导读(二):Gateway 控制平面 — 一条 WebSocket 连上所有人

在系列第一篇里我们看到,OpenClaw 的核心隐喻是 “Gateway 是控制平面”。它不存转储消息、不排队、不持久化事件流,而是一个长在 127.0.0.1:18789 上的状态化 RPC 服务器,所有客户端(CLI、macOS.app、iOS/Android Node、WebChat 浏览器、Canvas iframe)都靠一条 WebSocket 接入。 这一篇拆开控制平面的五个层面: 进程入口:CLI 怎么从 openclaw gateway 走到 Node 进程常驻 WebSocket 协议:握手帧、RPC 帧、Event 帧、状态快照与版本号 鉴权矩阵:Token / Password / Device Token / Bootstrap Token / Tailscale Whois / Trusted Proxy Pairing:陌生客户端怎么合法加入(setup code、pairing store、allowFrom) Session 模型:Session Key 组成规则、多 Agent 路由、Session ID 模糊匹配 注:下文所有 src/ 路径都指 openclaw/openclaw 仓库根下。 一、CLI 到 Gateway 的六段路径 第一篇概览过 src/entry.ts 和 src/cli/run-main.ts,这里按调用序重新画一遍,把每个关键函数在 graph 上标出来: flowchart TD A0["openclaw gateway --port 18789"] --> A1 A1["entry.tsmain guard + process.title"] --> A2 A2["normalizeEnv + enableCompileCache()"] --> A3 A3["ensureCliRespawnReady()按需 spawn 子 Node"] --> A4 A4["parseCliContainerArgsparseCliProfileArgs"] --> A5 A5["tryHandleRootVersionFastPathtryHandleRootHelpFastPath"] -->|miss| A6 A6["runMainOrRootHelp → runCli(argv)"] --> B0 B0["run-main.tscontainer? container in progress"] --> B1 B1["loadCliDotEnv (~/.openclaw/.env)"] --> B2 B2["assertSupportedRuntime (Node≥22.16)"] --> B3 B3["tryRouteCli(fast-path 子命令路由)"] -->|miss| B4 B4["buildProgram() + commander 注册"] --> B5 B5["installUnhandledRejectionHandler"] --> B6 B6["registerPluginCliCommandsFromValidatedConfig (lazy)"] --> B7 B7["program.parseAsync"] --> C0 C0["gateway 命令匹配 → gatewayCommandAction"] --> C1 C1["lock 文件 + 端口 acquire"] --> C2 C2["HTTP/WS server 启动"] --> C3 C3["channels/plugins/cron/canvas-host 启动"] --> C4["Ready"] 1. 为什么要自己 spawn 自己 ensureCliRespawnReady() 是 src/entry.ts 里最容易被忽视但非常重要的段落: ...

May 2, 2026

iOS中的内存管理

本文将系统介绍iOS中的内存管理机制,从程序内存布局、引用计数原理、ARC机制到系统级内存管理,帮助你建立完整的iOS内存管理知识体系。 一、程序内存布局 内存区域划分 在iOS应用程序中,内存按照用途和管理方式分为以下几个区域: 高地址 ┌─────────────────────────────────────┐ │ 栈区 (Stack) │ ↓ 向低地址增长 │ 局部变量、函数参数、返回地址 │ ├─────────────────────────────────────┤ │ ↕ │ │ 动态分配区 │ │ ↕ │ ├─────────────────────────────────────┤ │ 堆区 (Heap) │ ↑ 向高地址增长 │ 动态分配的对象实例 │ ├─────────────────────────────────────┤ │ 全局/静态区 (BSS) │ │ 未初始化的全局变量和静态变量 │ ├─────────────────────────────────────┤ │ 数据区 (Data) │ │ 已初始化的全局变量和静态变量 │ ├─────────────────────────────────────┤ │ 常量区 (Rodata) │ │ 字符串常量等 │ ├─────────────────────────────────────┤ │ 代码区 (Text) │ │ 编译后的代码 │ └─────────────────────────────────────┘ 低地址 各区域的特点: ...

May 2, 2026

OpenClaw 源码导读(四):Channels、Nodes 与扩展生态

前三篇我们看清了 OpenClaw 的三副骨架:架构全景、Gateway 控制平面、Agent Harness 执行管线。这一篇是压轴篇,我们看它的"血肉"——channels/、node-host/、canvas-host/、plugins/、skills/、sandbox/。OpenClaw 真正"好用"的感觉,是从这一层来的:一条消息从 WhatsApp 进来、触发 iOS 上的摄像头、在 Android 上渲染一张 Canvas、最后把结果发回 Slack 线程——整个过程中 Gateway 本身一行代码都不需要改。 本文涵盖 118 个 channel/provider extension、53 个 skill、iOS/macOS/Android node 客户端、A2UI Canvas 协议、以及 Docker sandbox 的整套隔离模型。 一、Channels:118 个 extension 是怎么"插"进来的 1. ChannelPlugin 接口 OpenClaw 所有 channel(微信、Telegram、Slack、iMessage、IRC、Discord……)本质上都是实现同一个接口 ChannelPlugin: export type ChannelPlugin<ResolvedAccount = any, Probe = unknown, Audit = unknown> = { id: ChannelId; meta: ChannelMeta; capabilities: ChannelCapabilities; defaults?: { queue?: { debounceMs?: number; }; }; reload?: { configPrefixes: string[]; noopPrefixes?: string[] }; setupWizard?: ChannelPluginSetupWizard; config: ChannelConfigAdapter<ResolvedAccount>; configSchema?: ChannelConfigSchema; setup?: ChannelSetupAdapter; pairing?: ChannelPairingAdapter; security?: ChannelSecurityAdapter<ResolvedAccount>; groups?: ChannelGroupAdapter; mentions?: ChannelMentionAdapter; outbound?: ChannelOutboundAdapter; status?: ChannelStatusAdapter<ResolvedAccount, Probe, Audit>; gatewayMethods?: string[]; gateway?: ChannelGatewayAdapter<ResolvedAccount>; auth?: ChannelAuthAdapter; approvalCapability?: ChannelApprovalCapability; elevated?: ChannelElevatedAdapter; commands?: ChannelCommandAdapter; lifecycle?: ChannelLifecycleAdapter; secrets?: ChannelSecretsAdapter; allowlist?: ChannelAllowlistAdapter; doctor?: ChannelDoctorAdapter; bindings?: ChannelConfiguredBindingProvider; conversationBindings?: ChannelConversationBindingSupport; streaming?: ChannelStreamingAdapter; threading?: ChannelThreadingAdapter; messaging?: ChannelMessagingAdapter; agentPrompt?: ChannelAgentPromptAdapter; directory?: ChannelDirectoryAdapter; resolver?: ChannelResolverAdapter; actions?: ChannelMessageActionAdapter; heartbeat?: ChannelHeartbeatAdapter; agentTools?: ChannelAgentToolFactory | ChannelAgentTool[]; }; 这个接口一共有 40 多个可选的 adapter,每个对应一种能力: ...

May 2, 2026

iOS 定时器的注意事项

iOS 开发中常用的定时器有 NSTimer、CADisplayLink 和 GCD Timer。它们各有特点,但在使用时都有一些需要注意的问题。 常见定时器类型 1. NSTimer 最常用的定时器,基于 RunLoop 实现: // 自动添加到当前 RunLoop 的 DefaultMode NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerFired) userInfo:nil repeats:YES]; // 手动创建,需要手动添加到 RunLoop NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerFired) userInfo:nil repeats:YES]; [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode]; 2. CADisplayLink 与屏幕刷新率同步的定时器,适合做动画: CADisplayLink *displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(update)]; [displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode]; 3. GCD Timer 不依赖 RunLoop,精度更高: dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue()); dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 1.0 * NSEC_PER_SEC, 0); dispatch_source_set_event_handler(timer, ^{ NSLog(@"GCD Timer fired"); }); dispatch_resume(timer); 注意事项一:循环引用问题 问题描述 NSTimer 和 CADisplayLink 在使用 target-action 模式时,会强引用 target。如果 target 又持有定时器,就会形成循环引用: ...

May 2, 2026

OpenClaw 源码导读(三):Agent Harness — 为"每一家 LLM 都能兜住"而生的执行管线

系列第一篇把 OpenClaw 的架构地图摊开,第二篇深入 Gateway 控制平面。这一篇,我们钻进整个项目的心脏——src/agents/,看 OpenClaw 怎么处理最复杂的一件事:当 Gateway 接到一条用户消息之后,怎么把它变成一轮真正的 LLM turn + Tool 循环。 这一层代码的体量和复杂度非常惊人: src/agents/pi-embedded-runner/run.ts 单文件 2160 行,是整个 agent 循环的驱动器。 src/agents/pi-embedded-runner/compact.ts 1148 行,专门处理上下文压缩。 整个 src/agents/ 目录有 830+ 个文件,光 anthropic-*.ts 就有近 20 个(为了 Messages API 的各种 edge case)。 同时支持 Anthropic、OpenAI、Google Gemini、Bedrock、Vertex、OpenRouter、Z.AI/GLM、MiniMax、Qwen、Ollama、Kilocode 等十多个 provider。 但这些"量"的背后,是一个只有一个接口的核心抽象:AgentHarness。本文就从这个抽象出发,一路看到 2160 行循环如何处置真实世界里十几类失败。 一、Harness 抽象:只有一个 runAttempt 1. 为什么不用 LangChain 风格的"Agent Framework" 读过 LangChain 或 CrewAI 的人第一眼会期望看到 AgentExecutor、AgentChain、Memory、Callback、Tool 这些类。OpenClaw 里一个都没有。 它的选择是:把整个 agent 循环当成一个黑盒,对外只暴露一个接口: export type AgentHarness = { id: string; label: string; pluginId?: string; supports(ctx: AgentHarnessSupportContext): AgentHarnessSupport; runAttempt(params: AgentHarnessAttemptParams): Promise<AgentHarnessAttemptResult>; compact?(params: AgentHarnessCompactParams): Promise<AgentHarnessCompactResult | undefined>; reset?(params: AgentHarnessResetParams): Promise<void> | void; dispose?(): Promise<void> | void; }; 五个方法,其中三个可选: ...

May 2, 2026

神经网络原理

想象你手写了一个数字"3",然后拿给计算机看——它是怎么"认出"这是3而不是8的?这个看似简单的问题,背后隐藏着整个深度学习的核心思想。让我们从最直觉的方式开始,一层一层揭开神经网络的面纱。 一、从像素到数字:问题的本质 1.1 手写数字识别 假设我们有一张28x28像素的灰度手写数字图片。每个像素是一个0到1之间的数字——0代表全黑,1代表全白。这意味着一张图片其实就是一个包含784个数字的列表。 图片 → [0.0, 0.0, 0.12, 0.85, 0.93, ..., 0.0] (784个数字) 我们的目标是设计一个函数,输入这784个数字,输出10个数字,分别代表这张图是0~9每个数字的概率: f(784个像素值) → [P(0), P(1), P(2), ..., P(9)] 比如输入一张手写的"3",我们期望输出类似: [0.01, 0.02, 0.05, 0.89, 0.01, 0.01, 0.00, 0.01, 0.00, 0.00] ^^^^ "3"的概率最高 这就是神经网络要解决的问题。但关键问题是:这个函数 f 长什么样? 1.2 为什么传统编程行不通 你可能会想:能不能写一堆if-else规则?比如"如果上方有一个圆弧,下方也有一个圆弧,那就是3"。 问题在于:人类写字的方式千变万化。同一个"3",有人写得圆润,有人写得尖锐,有人偏左,有人偏右。你几乎不可能用明确的规则覆盖所有情况。 我们需要的是一种从数据中自动学习规则的方法——这就是神经网络。 二、神经网络的结构 2.1 神经元:最基本的单元 让我们从最简单的单元开始。一个神经元本质上就是一个持有数字的容器。这个数字叫做激活值(activation),取值范围通常在0到1之间。 你可以把它想象成一个灯泡:0表示完全熄灭,1表示完全点亮,中间的值表示不同亮度。 2.2 层:神经元的组织方式 神经网络把神经元组织成层(layer): graph LR subgraph 输入层 [输入层 - 784个神经元] I1[像素1] I2[像素2] I3[...] I784[像素784] end subgraph 隐藏层1 [隐藏层1 - 16个神经元] H1_1[h1] H1_2[h2] H1_3[...] H1_16[h16] end subgraph 隐藏层2 [隐藏层2 - 16个神经元] H2_1[h1] H2_2[h2] H2_3[...] H2_16[h16] end subgraph 输出层 [输出层 - 10个神经元] O0[0] O1[1] O2[...] O9[9] end I1 --> H1_1 I1 --> H1_2 I784 --> H1_16 H1_1 --> H2_1 H1_16 --> H2_16 H2_1 --> O0 H2_16 --> O9 输入层:784个神经元,每个对应一个像素值 隐藏层:中间的层(这里用2层,每层16个神经元) 输出层:10个神经元,对应数字0~9 为什么叫"隐藏层"?因为在训练数据中,我们只知道输入(像素)和输出(标签),中间层在"学什么"我们并不直接指定——它们是被自动学出来的。 ...

May 2, 2026

责任链模式

定义 责任链模式(Chain of Responsibility Pattern)是一种行为型设计模式,它将请求的发送者和接收者解耦,让多个对象都有机会处理请求。将这些对象连成一条链,并沿着这条链传递请求,直到有一个对象处理它为止。 责任链模式的核心思想是:避免请求发送者与接收者耦合在一起,让多个对象都有可能接收请求,将这些对象组成一条链,并沿着这条链传递请求,直到有对象处理它为止。 为什么需要责任链模式 问题场景:假设我们正在开发一个审批系统,不同金额的报销需要不同级别的人员审批。 最直接的方式可能是这样: class ExpenseApproval { func approve(amount: Double) { if amount <= 1000 { // 组长审批 teamLeaderApprove(amount) } else if amount <= 5000 { // 部门经理审批 departmentManagerApprove(amount) } else if amount <= 10000 { // 总监审批 directorApprove(amount) } else { // CEO审批 ceoApprove(amount) } } func teamLeaderApprove(_ amount: Double) { ... } func departmentManagerApprove(_ amount: Double) { ... } func directorApprove(_ amount: Double) { ... } func ceoApprove(_ amount: Double) { ... } } 这种方式有什么问题? ...

May 2, 2026

反向传播算法

在上一篇文章中,我们构建了一个神经网络,了解了前向传播是如何产生预测的,也知道了损失函数可以衡量预测有多差。但我们遗留了最核心的问题:那13,002个参数到底该怎么调整,才能让网络变得更好? 这就是反向传播算法(Backpropagation)要回答的问题。它是神经网络学习的核心引擎,几乎所有现代深度学习的训练都建立在它之上。 一、梯度下降:从直觉开始 1.1 一个简单的类比 想象你站在一片浓雾中的山地上,看不到全貌,但你能感受到脚下地面的倾斜方向。你的目标是找到最低的山谷。最自然的策略是什么? 顺着脚下最陡的下坡方向走一小步。 这就是**梯度下降(Gradient Descent)**的核心思想。 1.2 数学表述 假设损失函数 $L$ 取决于所有参数 $\theta = (w_1, w_2, ..., w_n, b_1, b_2, ..., b_m)$。 梯度是一个向量,指向损失增长最快的方向: $$\nabla L = \left(\frac{\partial L}{\partial w_1}, \frac{\partial L}{\partial w_2}, \cdots, \frac{\partial L}{\partial b_m}\right)$$梯度的每个分量告诉我们:如果微微增大某个参数,损失会增加多少。 要降低损失,我们朝梯度的反方向迈步: $$\theta_{\text{new}} = \theta_{\text{old}} - \eta \cdot \nabla L$$其中 $\eta$ 是学习率(learning rate),控制每一步的步长。 1.3 学习率的微妙之处 学习率 下降行为 结果 太小 每步挪动极小,到达局部最小值后就停住了,无法跳出 收敛极慢,可能困在局部最小值 太大 一步跨过了最低点,又跨回来,反复来回 在最低点附近震荡,甚至发散 合适 每步大小适中,沿着曲面稳步走向谷底 稳步下降,逐渐逼近最优解 1.4 梯度下降的关键问题 现在整个问题归结为一件事:怎么高效地计算 $\nabla L$,即损失对每一个参数的偏导数? 你可能会想:对每个参数微微扰动一下,看看损失变化了多少。但如果有13,002个参数,每个参数都扰动一次,就需要做13,002次前向传播——太慢了。 ...

May 2, 2026

Swift二进制兼容性

Swift二进制兼容性是指Swift编译后的二进制代码能够跨版本、跨模块正确链接和运行的能力。它由三个核心机制组成:ABI稳定(Swift 5.0)、模块稳定性(Swift 5.1)和Library Evolution。这三者共同使得Swift二进制框架的分发成为可能。 graph TD A[Swift二进制兼容性] --> B[ABI稳定Swift 5.0] A --> C[模块稳定性Swift 5.1] A --> D[Library Evolution] B --> B1[统一运行时固定调用约定/内存布局] C --> C1[.swiftinterface跨编译器版本导入模块] D --> D1[运行时布局查询库可独立于客户端更新] B1 --> E[二进制框架分发] C1 --> E D1 --> E ABI稳定 什么是ABI ABI(Application Binary Interface,应用程序二进制接口)定义了二进制层面的接口规范,包括: 数据类型的大小和对齐方式:如Int在64位系统上是8字节 函数调用约定:参数如何传递、返回值如何获取 名称修饰(Name Mangling)规则:函数和类型在二进制中的命名方式 内存布局:对象在内存中的结构 异常处理机制:错误如何在二进制层面传播 运行时元数据格式:类型信息的存储方式 ABI vs API 特性 API(源码接口) ABI(二进制接口) 层面 源代码层面 编译后的二进制层面 兼容性检查 编译时 链接时/运行时 关注点 函数签名、类型定义 内存布局、调用约定 变化影响 需要重新编译 需要重新链接或替换二进制 一个简单的例子: // API层面:函数签名 func calculate(value: Int) -> Int // ABI层面关注的是: // - Int在内存中占多少字节 // - value参数通过哪个寄存器传递 // - 返回值通过哪个寄存器返回 // - 函数在二进制中的符号名是什么 ABI不稳定时期的问题 在Swift 5.0之前,Swift的ABI是不稳定的。这意味着: ...

May 2, 2026

Swift中import详解

源码版本说明:本文涉及的源码基于 Swift 编译器 开发版本(swift main分支,commit: e0db5152d4d4,2026-01-19)。不同版本的实现细节可能略有差异,但核心机制保持一致。 在前一篇文章Objective-C中import详解中,我们深入探讨了 Objective-C 中的 import 机制,包括 #include、#import、@import 以及 Clang Modules 的工作原理。本文将继续从 Swift 编译器的角度,讲解 Swift 中 import 的语法、模块查找机制、底层实现原理以及与 Objective-C 的互操作。 从 Clang Module 到 Swift Module 在深入 Swift 的 import 机制之前,让我们先回顾一下上一篇文章中 Clang Module 的核心概念。 Clang Module 核心概念回顾 在 Objective-C 文章中,我们学习了 Clang Module 的几个关键特性: module.modulemap:定义模块结构的配置文件 framework module Foundation { umbrella header "Foundation.h" export * } 预编译模块缓存(.pcm 文件):Clang 将模块编译为二进制格式,缓存以加速后续编译 懒加载机制:只加载实际使用的声明,未使用的部分不会被解析 自动链接:@import 会自动链接对应的库,无需手动配置 模块查找路径:通过 -I、-F 等参数指定搜索路径 Swift 如何继承 Clang Module 的设计 Swift 的模块系统并非从零开始设计,而是站在 Clang Module 的肩膀上,继承了其核心优势,并针对 Swift 的特性进行了扩展: ...

May 2, 2026