OpenClaw 源码分析
OpenClaw 源码分析
面向前端开发者的源码拆解笔记,通过「逐个击破」方式深入理解 OpenCLAW 在架构设计、状态管理、LLM 交互模式、插件系统、流式处理等方面的创新点。
- openclaw
Written by
OpenClaw 源码分析
面向前端开发者的源码拆解笔记,通过「逐个击破」方式深入理解 OpenCLAW 在架构设计、状态管理、LLM 交互模式、插件系统、流式处理等方面的创新点。
Written by
面向前端开发者的源码拆解笔记,通过「逐个击破」方式深入理解 OpenCLAW 在架构设计、状态管理、LLM 交互模式、插件系统、流式处理等方面的创新点。
传统项目常用 Jest + Prettier + ESLint + npm;OpenClaw 采用一套更「新」的工具链,值得单独了解。
| 工具 | 用途 | 传统替代 |
|---|---|---|
| pnpm | 包管理 + workspace | npm / yarn |
| pnpm-workspace.yaml | Monorepo 定义 | lerna / npm workspaces |
Code
# pnpm-workspace.yaml
packages:
- .
- ui
- packages/*
- extensions/*ui、packages/*、extensions/* 均为 workspace 成员onlyBuiltDependencies 控制部分原生依赖的构建| 工具 | 用途 | 传统替代 |
|---|---|---|
| Vitest | 单元/集成/E2E 测试 | Jest / Mocha |
多配置拆分:不同场景用不同 vitest 配置
| 配置 | 用途 |
|---|---|
vitest.unit.config.ts | 单元测试、覆盖率 |
vitest.gateway.config.ts | Gateway 相关(--pool=forks 隔离) |
vitest.channels.config.ts | 通道逻辑 |
vitest.extensions.config.ts | 插件 |
vitest.e2e.config.ts | E2E |
vitest.live.config.ts | 需真实 API 的 live 测试 |
ui/vitest.config.ts | UI 测试(含 @vitest/browser-playwright) |
*.test.ts、*.e2e.test.ts@vitest/coverage-v8| 工具 | 用途 | 传统替代 |
|---|---|---|
| Oxlint | Lint(Rust 实现,快) | ESLint |
| Oxfmt | 格式化 | Prettier |
Code
pnpm lint # oxlint --type-aware
pnpm format # oxfmt --write
pnpm check # lint + format check + tsgo.oxfmtrc.jsonc:格式化规则(tabWidth、ignorePatterns、experimentalSortImports 等)--type-aware 做更细的规则检查| 工具 | 用途 |
|---|---|
| tsx | 直接运行 TS 脚本(node --import tsx) |
| Jiti | 运行时加载 TS 模块(插件系统用) |
| Bun | 可选,用于 bun <file.ts>、bunx |
node --import tsx scripts/xxx.ts,无需先编译import,支持 TS 源码直接加载| 维度 | OpenClaw | 常见传统方案 |
|---|---|---|
| 包管理 | pnpm workspace | npm / yarn |
| 测试 | Vitest 多配置 | Jest |
| Lint | Oxlint | ESLint |
| 格式化 | Oxfmt | Prettier |
| 脚本执行 | tsx / Bun | ts-node / 编译后 node |
| 插件加载 | Jiti | 预编译 / ts-node |
Code
openclaw/
├── src/ # 核心源码(Node/TS)
├── ui/ # Control UI(Vite + Lit,workspace 包)
├── extensions/ # 插件(每个是 workspace 包)
├── apps/ # 原生应用(macos, ios, android, shared)
├── docs/ # 文档(Mintlify)
├── test/ # 测试 fixture、helper(非 *.test.ts)
├── packages/ # 共享包(若有)
├── vendor/ # 第三方 vendored 代码(如 a2ui)
└── scripts/ # 构建、发布等脚本| 类别 | 目录 | 职责 |
|---|---|---|
| 入口 | entry.ts, index.ts | 程序入口、导出 |
| CLI | cli/, commands/ | 命令行解析、子命令实现 |
| 网关 | gateway/ | WebSocket 服务、协议、请求分发 |
| Agent | agents/ | LLM 调用、工具执行、流式、Pi 集成 |
| 通道 | telegram/, discord/, slack/, signal/, imessage/, web/, line/ | 各通道的 monitor、send、account 等 |
| 通道抽象 | channels/ | 通道注册表、dock、插件接口、路由策略 |
| 路由 | routing/ | sessionKey 解析、路由决策 |
| 自动回复 | auto-reply/ | 触发、回复分发、block streaming |
| 插件 | plugins/, plugin-sdk/ | 发现、加载、注册、SDK 类型 |
| 配置 | config/ | 配置加载、校验、schema |
| 会话 | sessions/ | 会话存储、sessionKey 工具 |
| 基础设施 | infra/ | 文件、进程、网络等底层能力 |
| 媒体 | media/, media-understanding/ | 图片、音频处理 |
| 其他 | hooks/, memory/, pairing/, security/, tts/, wizard/ 等 | 各领域能力 |
Code
src/channels/ ← 抽象层:registry、dock、plugins 接口
├── dock.ts # 通道能力描述(capabilities、config、streaming)
├── registry.ts # 通道注册表、顺序
└── plugins/ # onboarding、status-issues、group-mentions 等
src/telegram/ ← 内置实现:Telegram 特有逻辑
src/discord/ ← 内置实现
src/slack/
...
extensions/msteams/ ← 插件实现:通过 registerChannel 注册channels/dock.ts 中注册registerChannel 注册Code
ui/src/
├── main.ts # 入口
├── styles/ # 全局样式
├── i18n/ # 国际化
└── ui/
├── app.ts # 根组件、状态
├── app-*.ts # 各功能模块(gateway、chat、render、settings)
├── controllers/ # 业务逻辑(chat、config、agents、sessions 等)
├── views/ # 按 tab 的视图(chat、config、overview 等)
├── chat/ # 聊天相关(markdown、message-extract)
└── types/ # 类型定义Code
extensions/diffs/
├── package.json # openclaw.extensions: ["./index.ts"]
├── index.ts # 入口,export default { id, register }
└── src/ # 插件内部实现package.json 的 dependenciesopenclaw 放 devDependencies 或 peerDependenciesCode
apps/
├── macos/ # macOS 菜单栏应用(SwiftUI)
├── ios/ # iOS 应用
├── android/ # Android 应用
└── shared/ # 共享代码(如 OpenClawKit)channels/ 抽象,telegram/ 等实现,插件扩展*.test.ts 放在对应模块旁| 模块 | 技术栈 | 路径 | 说明 |
|---|---|---|---|
| Control UI | Vite + Lit | ui/ | 浏览器端管理界面,通过 WebSocket 连接 Gateway |
| Gateway | Node.js + WebSocket | src/gateway/ | 核心网关,统一管理所有消息通道与 Agent 调用 |
| 插件系统 | Jiti + 动态加载 | src/plugins/ | 插件发现、加载、注册、运行时 |
| Agent 运行时 | Pi Agent Core | src/agents/ | LLM 调用、工具执行、流式输出 |
| 原生应用 | SwiftUI / Kotlin | apps/macos/, apps/ios/, apps/android/ | 同样通过 WebSocket 连接 Gateway |
Code
┌─────────────────────────────────────────────────────────────────┐
│ Clients (Control UI / Mac App / CLI / 移动端) │
│ └─ WebSocket 连接 ─────────────────────────────────────────────┼──┐
└─────────────────────────────────────────────────────────────────┘ │
│
┌─────────────────────────────────────────────────────────────────┐ │
│ Gateway (单例,默认 127.0.0.1:18789) │ │
│ ├─ WebSocket Server (req/res + event 协议) │◄─┘
│ ├─ 消息通道 (WhatsApp/Telegram/Discord/Slack/Signal/WebChat) │
│ ├─ Agent 调度 (chat.send, agent 请求) │
│ └─ 插件注册 (channels, tools, gateway handlers) │
└─────────────────────────────────────────────────────────────────┘理由:
ui/ 使用 Lit + Vite,与常见 SPA 技术栈接近app-view-state.ts 是 UI 的「单一数据源」,结构清晰pnpm dev 即可在浏览器中体验,便于边看边改Code
第 1 步:Control UI 状态与渲染
└─ ui/src/ui/app.ts
└─ ui/src/ui/app-view-state.ts
└─ ui/src/ui/app-render.ts
第 2 步:Gateway WebSocket 通信
└─ ui/src/ui/gateway.ts
└─ ui/src/ui/app-gateway.ts
└─ docs/concepts/architecture.md
第 3 步:Chat 流程(发消息 → 流式响应)
└─ ui/src/ui/controllers/chat.ts
└─ ui/src/ui/app-chat.ts
└─ src/gateway/server-chat.ts
第 4 步:流式处理与 Block Streaming
└─ docs/concepts/streaming.md
└─ src/agents/openai-ws-stream.ts
└─ src/gateway/server-chat.ts (createAgentEventHandler)
第 5 步:插件系统
└─ src/plugins/loader.ts
└─ src/plugins/discovery.ts
└─ src/plugins/registry.ts
└─ extensions/* (示例插件)connect,之后 req/res + event 双向通信docs/concepts/architecture.md、docs/gateway/protocol.md@state() + 集中式 AppViewState,无 Redux/Zustandapp-view-state.ts(类型定义)、app.ts(状态持有与更新)agent 请求 → Gateway 调度 → Pi Agent 执行event:agent 推送 text_delta、tool_* 等事件discoverOpenClawPlugins 扫描 extensions/ 与配置路径loadOpenClawPlugins → Jiti 动态加载 → 注册 channels/tools/handlersdocs/concepts/streaming.mdOpenClaw Control UI 采用 「组件即状态容器」 模式,没有 Redux/Zustand 等外部状态库:
Code
OpenClawApp (LitElement)
├─ 100+ 个 @state() 属性 ← 所有 UI 状态集中在此
├─ render() → renderApp(this) ← 将自身作为 state 传入
└─ 各种 handle* 方法 ← 用户操作 → 更新 @state → 触发重渲染关键代码(app.ts):
Code
@customElement("openclaw-app")
export class OpenClawApp extends LitElement {
@state() connected = false;
@state() chatMessage = "";
@state() chatStream: string | null = null;
// ... 100+ @state()
render() {
return renderApp(this as unknown as AppViewState); // 自身即 state
}
}| 文件 | 职责 |
|---|---|
app.ts | 状态持有(@state)、生命周期、事件处理入口 |
app-view-state.ts | AppViewState 类型定义,描述「状态 + 方法」的完整接口 |
app-render.ts | 纯函数 renderApp(state),根据 state 生成 Lit 模板 |
AppViewState 不仅是数据,还包含所有 handler:
Code
// app-view-state.ts 片段
export type AppViewState = {
// 数据字段
connected: boolean;
chatMessage: string;
chatStream: string | null;
// ...
} & {
// 方法(由 app.ts 实现,通过 this 传入)
connect: () => void;
handleSendChat: (messageOverride?: string, opts?: {...}) => Promise<void>;
handleAbortChat: () => Promise<void>;
// ...
};这样 renderApp(state) 可以统一通过 state.handleSendChat 绑定到按钮,无需在组件树中逐层传递。
renderApp 是唯一入口,按 tab 分发到不同子视图:
Code
// app-render.ts 简化逻辑
export function renderApp(state: AppViewState) {
// 根据 state.tab 渲染不同内容
// chat | config | overview | agents | sessions | usage | cron | skills | logs | debug | instances | nodes
return html`
<header>...</header>
<main>
${state.tab === "chat" ? renderChat(state, ...) : nothing}
${state.tab === "config" ? renderConfig(state, ...) : nothing}
// ...
</main>
`;
}子视图(views/chat.ts、views/config.ts 等)接收同一个 state,实现「单一数据源」。
Code
用户点击发送
→ handleSendChat() 被调用
→ 内部设置 this.chatSending = true
→ Lit 检测到 @state() 变化
→ render() 被重新执行
→ renderApp(this) 用最新 state 生成新 DOM
→ Lit 做 diff 并更新真实 DOMcreateRenderRoot() { return this; },直接渲染到宿主元素,便于全局样式controllers/(如 chat.ts、config.ts),app 只做调用与状态更新AppViewState 保证 render 函数拿到的 state 结构完整Gateway 使用 WebSocket 文本帧 + JSON,三种帧类型:
| 帧类型 | 方向 | 格式 |
|---|---|---|
req | Client → Gateway | {type:"req", id, method, params} |
res | Gateway → Client | {type:"res", id, ok, payload|error} |
event | Gateway → Client | {type:"event", event, payload, seq?} |
关键规则:首帧必须是 connect 请求,否则连接被关闭。
Code
1. WebSocket 建立
2. Gateway 可能先发 event: connect.challenge(含 nonce)
3. Client 发 req: connect(含 device 签名、auth token/password)
4. Gateway 回 res: hello-ok(含 snapshot、deviceToken 等)
5. 之后可正常 req/res + 接收 eventgateway.ts 中 sendConnect() 负责组装 connect 参数:device 身份、auth、client 信息等。
Code
// gateway.ts
export class GatewayBrowserClient {
private ws: WebSocket | null = null;
private pending = new Map<string, Pending>(); // id → {resolve, reject}
request<T>(method: string, params?: unknown): Promise<T> {
const id = generateUUID();
this.pending.set(id, { resolve, reject });
this.ws.send(JSON.stringify({ type: "req", id, method, params }));
return promise; // 等待 res 时 resolve/reject
}
// 消息分发
private handleMessage(raw: string) {
if (frame.type === "event") → opts.onEvent?.(evt);
if (frame.type === "res") → pending.get(id).resolve(payload);
}
}pending Map 匹配 idonEvent 回调推送给上层,无 req 对应connectGateway(host) 创建 GatewayBrowserClient,并挂载回调:
| 回调 | 作用 |
|---|---|
onHello | 连接成功 → 更新 host.connected、host.hello,拉取 agents/nodes/devices,刷新当前 tab |
onClose | 断开 → 设置 host.lastError,非 1012 时展示错误;可触发自动重连 |
onEvent | 收到 event → 调用 handleGatewayEvent(host, evt) |
onGap | 检测到 event seq 空洞 → 提示刷新 |
根据 evt.event 分发到不同处理逻辑:
| event | 处理 |
|---|---|
agent | handleAgentEvent → 更新 tool stream、text delta;tool result 后可能 reload history |
chat | handleChatGatewayEvent → handleChatEvent,更新 chatMessages/chatStream;final 时 reload history |
presence | 更新 host.presenceEntries |
cron | 若在 cron tab,则 loadCron |
device.pair.* | loadDevices |
exec.approval.requested | 加入 host.execApprovalQueue |
exec.approval.resolved | 从 queue 移除 |
update.available | 更新 host.updateAvailable |
AUTH_* 类错误:scheduleReconnect(),指数退避(800ms → 1.36s → … 上限 15s)AUTH_TOKEN_MISSING 等不可恢复错误:不重连,需用户操作Code
用户操作 (如发送消息)
→ host.handleSendChat()
→ client.request("agent", { sessionKey, message, ... })
→ Gateway 执行 Agent
→ Gateway 推送 event: agent (text_delta, tool_*, lifecycle)
→ onEvent → handleGatewayEvent → handleAgentEvent
→ 更新 host.chatStream / host.chatToolMessages / toolStreamById
→ @state 变化 → render() → UI 更新Code
用户输入 + 点击发送
→ handleSendChat (app-chat.ts)
→ sendChatMessageNow
→ sendChatMessage (controllers/chat.ts)
→ client.request("chat.send", { sessionKey, message, deliver: false, idempotencyKey: runId })
→ Gateway 接收 chat.send,内部启动 Agent 运行
→ Gateway 推送 event: chat (delta/final/aborted/error)
→ Gateway 推送 event: agent (tool/compaction/fallback/lifecycle)
→ handleChatEvent / handleAgentEvent 更新 UI 状态注意:前端调用的是 chat.send,不是 agent。chat.send 是 WebChat 专用入口,Gateway 内部会调度 Agent。
Code
// controllers/chat.ts
export async function sendChatMessage(state, message, attachments?) {
// 1. 立即乐观更新:把 user 消息加入 chatMessages
state.chatMessages = [...state.chatMessages, { role: "user", content: [...], timestamp }];
// 2. 设置流式状态
state.chatSending = true;
state.chatRunId = runId; // 客户端生成的 UUID,用作 idempotencyKey
state.chatStream = "";
state.chatStreamStartedAt = now;
// 3. 发送请求(不等待 Agent 完成)
await state.client.request("chat.send", {
sessionKey, message, deliver: false, idempotencyKey: runId, attachments
});
// 4. 请求成功即返回,后续通过 event 推送
return runId;
}| 事件类型 | 用途 | 处理函数 |
|---|---|---|
event: chat | 文本流式(delta)与终态(final/aborted/error) | handleChatEvent |
event: agent | 工具调用、compaction、模型 fallback | handleAgentEvent |
Code
// ChatEventPayload: { runId, sessionKey, state, message?, errorMessage? }
// state: "delta" | "final" | "aborted" | "error"
if (payload.state === "delta") {
// 增量文本:累积到 chatStream
const next = extractText(payload.message);
if (next.length >= (state.chatStream ?? "").length) {
state.chatStream = next;
}
} else if (payload.state === "final") {
// 完成:写入 chatMessages,清空流式状态
state.chatMessages = [...state.chatMessages, finalMessage];
state.chatStream = null;
state.chatRunId = null;
}
// aborted / error 类似,保留已流式内容或设置 lastErrorchatMessages,并清空 chatStream、chatRunId仅处理 payload.stream === "tool" 的 agent 事件:
Code
// AgentEventPayload: { runId, seq, stream, ts, sessionKey?, data }
// data: { toolCallId, name, phase, args?, partialResult?, result? }
// phase: "start" | "update" | "result"
// 新工具调用时:把当前 chatStream 提交为 segment,显示在工具卡片上方
if (!entry) {
if (host.chatStream?.trim()) {
host.chatStreamSegments = [...host.chatStreamSegments, { text: host.chatStream, ts }];
host.chatStream = null; // 清空,后续文本流在工具下方
}
entry = { toolCallId, name, args, output, ... };
host.toolStreamById.set(toolCallId, entry);
host.toolStreamOrder.push(toolCallId);
}
// update/result:更新 entry.output设计要点:工具调用到来时,先把已流式文本固化为 chatStreamSegments,这样「文本 → 工具卡片 → 更多文本」的展示顺序正确。
flushChatQueue 发送下一条handleAbortChat → chat.abortrefreshSessions 后发送,用于切换会话| 字段 | 含义 |
|---|---|
chatMessages | 已持久化的消息列表(含 user + assistant) |
chatStream | 当前 run 的实时流式文本 |
chatStreamSegments | 工具调用前的文本片段(工具卡片上方) |
chatToolMessages | 工具调用卡片(由 toolStreamById 同步生成) |
chatRunId | 当前 run 的客户端 id,用于匹配 event |
OpenClaw 的流式分为两个独立层次,均面向外部通道(Telegram/Discord/Slack 等),不包含 WebChat:
| 层 | 用途 | 说明 |
|---|---|---|
| Block Streaming | 按块发送真实消息 | 粗粒度块,非 token 级;每条是完整 channel 消息 |
| Preview Streaming | 生成中更新预览气泡 | 单条临时消息的 send + edit/append |
重要:目前没有真正的 token-delta 流式到外部通道;Preview 是「消息级」的 send + edit。
Code
Model text_delta/events
└─ EmbeddedBlockChunker.append()
├─ blockStreamingBreak = "text_end"
│ └─ 缓冲区达到条件 → drain() → 发出块 → channel send
└─ blockStreamingBreak = "message_end"
└─ 等 message_end → drain(force) → 一次性或分块发送位置:src/agents/pi-embedded-block-chunker.ts
minChars 才考虑发出(除非 force 或 flushOnParagraph)maxChars 前分割,必要时在 maxChars 处硬切paragraph → newline → sentence → whitespace → hard breakmaxChars 切时,会关闭并重新打开 fence 保持 Markdown 合法Code
// 典型用法
const chunker = new EmbeddedBlockChunker({
minChars: 800,
maxChars: 1200,
breakPreference: "paragraph",
flushOnParagraph?: true, // chunkMode="newline" 时
});
chunker.append(textDelta);
chunker.drain({ force: isMessageEnd, emit: (chunk) => onBlockReply(chunk) });启用 Block Streaming 时,可先合并多个块再发送,减少「单行刷屏」:
idleMs:空闲一段时间后再 flushmaxChars:超过则强制 flushminChars:不足则继续累积(最终 flush 会发完剩余)joiner:由 breakPreference 推导(paragraph → \n\n,newline → \n,sentence → 空格)Signal/Slack/Discord 默认 coalesce minChars 提高到 1500。
| 配置项 | 位置 | 说明 |
|---|---|---|
blockStreamingDefault | agents.defaults | "on" / "off"(默认 off) |
blockStreamingBreak | agents.defaults | "text_end" / "message_end" |
blockStreamingChunk | agents.defaults | { minChars, maxChars, breakPreference? } |
blockStreamingCoalesce | agents.defaults | { minChars?, maxChars?, idleMs? } |
*.blockStreaming | 各 channel | 强制该通道 on/off |
*.textChunkLimit | 各 channel | 单条消息字符上限 |
*.chunkMode | 各 channel | "length" / "newline"(按段落分) |
配置:channels.<channel>.streaming
| 模式 | 行为 |
|---|---|
off | 关闭预览流式 |
partial | 单条预览,用最新文本替换 |
block | 分块追加式预览 |
progress | Slack 专用:进度状态 → 最终答案 |
通道支持:Telegram、Discord、Slack 均支持 partial/block;Slack 额外支持 progress。
注意:通道开启 blockStreaming 时,会跳过 Preview Streaming,避免重复流式。
event: chat(delta/final)和 event: agent(tool)接收流式数据handleChatEvent 中累积到 chatStreamCode
discoverOpenClawPlugins() → 扫描候选
↓
loadPluginManifestRegistry() → 解析 manifest,决定启用/禁用
↓
Jiti 动态加载入口模块 → 执行 register(api)
↓
createApi 提供的 register* 方法 → 注册到 PluginRegistry
↓
setActivePluginRegistry() → 全局生效扫描顺序与来源:
| 来源 | 路径 | origin |
|---|---|---|
| 配置 | plugins.load.paths | config |
| 工作区 | workspace/.openclaw/extensions | workspace |
| 内置 | resolveBundledPluginsDir()(extensions/) | bundled |
| 全局 | ~/.config/openclaw/extensions | global |
候选结构:PluginCandidate { idHint, source, rootDir, origin, packageName?, ... }
安全:检查路径是否逃逸 root、权限、所有权;非 Windows 下校验 uid。
package.json 中通过 openclaw.extensions 声明入口:
Code
{
"name": "@openclaw/diffs",
"openclaw": {
"extensions": ["./index.ts"]
}
}loadPluginManifestRegistry 会解析 configSchema、kind、configUiHints 等,并结合 plugins.allow、plugins.entries[id].enabled 决定是否启用。
Code
// 典型结构(如 extensions/diffs/index.ts)
const plugin = {
id: "diffs",
name: "Diffs",
description: "Read-only diff viewer...",
configSchema: diffsPluginConfigSchema, // 可选
register(api: OpenClawPluginApi) {
api.registerTool(createDiffsTool({ api, store }));
api.registerHttpRoute({ path: "/plugins/diffs", auth: "plugin", handler: ... });
api.on("before_prompt_build", async () => ({ prependSystemContext: "..." }));
},
};
export default plugin;| 注册类型 | 方法 | 用途 |
|---|---|---|
| 工具 | registerTool | Agent 可调用的工具 |
| 通道 | registerChannel | 新消息通道(如 msteams、matrix) |
| 提供者 | registerProvider | 新 LLM/连接提供者 |
| Gateway 方法 | registerGatewayMethod | 扩展 WebSocket API |
| HTTP 路由 | registerHttpRoute | 插件专属 HTTP 端点 |
| Hooks | registerHook / api.on | 生命周期钩子(before_agent_start 等) |
| CLI | registerCli | 扩展 CLI 子命令 |
| 服务 | registerService | 后台服务 |
| 命令 | registerCommand | 斜杠命令等 |
openclaw/plugin-sdk → 本地 src/plugin-sdk,插件用 import ... from "openclaw/plugin-sdk/..." 解析到源码PluginRuntime 用 Proxy 延迟初始化,避免启动时加载所有通道依赖registryCache 按 workspaceDir + pluginsConfig 缓存,避免重复加载Gateway 启动时调用 loadGatewayPlugins:
Code
// src/gateway/server-plugins.ts
const { pluginRegistry, gatewayMethods } = loadGatewayPlugins({
cfg,
workspaceDir,
log,
coreGatewayHandlers,
baseMethods,
});
// gatewayMethods = baseMethods + pluginRegistry.gatewayHandlers 的 key插件注册的 gatewayHandlers 会合并进 Gateway 的请求处理方法表,从而扩展 WebSocket API。
PluginRegistry,便于 Gateway/CLI/Agent 消费api,register* 会绑定 pluginId,避免冲突plugins.allow 可限制只加载白名单插件,提高安全性