适用版本: OpenClaw v2026.3 阅读时间: 约 30 分钟 前置知识: 阅读前三篇文章,理解架构和消息流程 源码位置:
src/plugins/,extensions/,skills/
开篇:为什么需要扩展?
前三篇文章,我们深入理解了 OpenClaw 的核心架构:
- Gateway——控制平面
- 消息流程——从接收到回复
- Agent——AI 的躯壳与灵魂
这些是 OpenClaw 的"骨架"。但要成为一个真正有用的助手,它需要"肌肉"——扩展。
本文回答核心问题:
OpenClaw 的哪些部分可以定制?如何优雅地扩展而不修改核心代码?
扩展点概览
flowchart TB
subgraph Core["核心系统"]
GW[Gateway]
SM[Session Manager]
AR[Agent Runtime]
end
subgraph Extensions["扩展点"]
CP[Channel Plugins<br/>渠道适配]
TOOLS[Custom Tools<br/>自定义工具]
HOOKS[Hooks<br/>生命周期钩子]
SKILLS[Skills<br/>技能包]
end
subgraph External["外部集成"]
WA[WhatsApp]
TG[Telegram]
API[Custom API]
WEB[Webhook]
end
External --> CP
CP --> GW
GW --> HOOKS
AR --> TOOLS
HOOKS --> SKILLS
style Extensions fill:#e8f5e9
| 扩展点 | 用途 | 开发难度 |
|---|---|---|
| Channel Plugin | 接入新的消息平台 | 中等 |
| Custom Tool | 扩展 Agent 能力 | 简单 |
| Hook | 介入生命周期事件 | 中等 |
| Skill | 封装复杂能力 | 中等 |
Channel Plugin:接入新渠道
问题:如何支持一个新的消息平台?
假设你要为 OpenClaw 添加 Signal 支持。
步骤 1:创建插件目录
~/.openclaw/plugins/signal/
├── openclaw.plugin.yaml # 插件清单
├── index.ts # 入口文件
├── channel.ts # Channel 实现
└── types.ts # 类型定义
步骤 2:编写插件清单
# ~/.openclaw/plugins/signal/openclaw.plugin.yaml
id: signal
name: Signal Channel
version: 1.0.0
description: Signal messaging channel support
# 入口文件
main: ./index.js
# 提供的能力
provides:
- channel:signal
# 依赖
dependencies:
- signal-cli # 外部依赖
# 配置 Schema
config:
type: object
properties:
phoneNumber:
type: string
description: Signal phone number
dataDir:
type: string
description: Signal data directory
required:
- phoneNumber
步骤 3:实现 Channel 接口
// ~/.openclaw/plugins/signal/channel.ts
import {
ChannelPlugin,
ChannelId,
ChannelContext,
NormalizedMessage,
SendMessageParams,
MessageCallback
} from "@openclaw/channel-types";
export class SignalChannel implements ChannelPlugin {
id: ChannelId = "signal";
private config: SignalConfig;
private messageCallbacks: MessageCallback[] = [];
private signalCli: SignalCli;
async start(context: ChannelContext): Promise<void> {
this.config = context.config as SignalConfig;
// 1. 初始化 signal-cli
this.signalCli = new SignalCli({
phoneNumber: this.config.phoneNumber,
dataDir: this.config.dataDir,
});
// 2. 监听消息
this.signalCli.on("message", async (rawMsg) => {
const normalized = this.normalizeMessage(rawMsg);
for (const cb of this.messageCallbacks) {
await cb(normalized);
}
});
// 3. 启动接收循环
await this.signalCli.startReceive();
}
async stop(): Promise<void> {
await this.signalCli.stop();
}
async sendMessage(params: SendMessageParams): Promise<void> {
await this.signalCli.sendMessage({
recipient: params.to,
message: params.text,
attachments: params.attachments?.map(a => a.path),
});
}
onMessage(callback: MessageCallback): void {
this.messageCallbacks.push(callback);
}
private normalizeMessage(rawMsg: SignalMessage): NormalizedMessage {
return {
id: rawMsg.timestamp.toString(),
channelId: "signal",
accountId: this.config.phoneNumber,
senderId: rawMsg.sender,
senderName: rawMsg.senderName,
text: rawMsg.message,
timestamp: rawMsg.timestamp,
attachments: rawMsg.attachments?.map(a => ({
type: a.contentType,
data: a.data,
filename: a.filename,
})),
isGroup: rawMsg.groupId !== undefined,
groupId: rawMsg.groupId,
};
}
}
步骤 4:注册插件
// ~/.openclaw/plugins/signal/index.ts
export { SignalChannel } from "./channel";
// 插件导出
export default {
channels: [SignalChannel],
};
步骤 5:配置启用
# ~/.openclaw/openclaw.yaml
channels:
signal:
enabled: true
phoneNumber: "+1234567890"
dataDir: ~/.signal
plugins:
enabled:
- signal
插件生命周期
sequenceDiagram
autonumber
participant Gateway as Gateway
participant Loader as Plugin Loader
participant Signal as Signal Channel
Note over Gateway: 启动阶段
Gateway->>Loader: loadPlugins()
Loader->>Signal: new SignalChannel()
Loader->>Signal: start(context)
Note over Signal: 运行阶段
Signal-->>Gateway: onMessage(msg)
Gateway->>Signal: sendMessage(params)
Note over Gateway: 关闭阶段
Gateway->>Signal: stop()
Custom Tool:扩展 Agent 能力
问题:如何让 Agent 执行自定义操作?
假设你要让 Agent 能够查询公司内部的 Jira 系统。
方式一:通过配置定义工具
# ~/.openclaw/openclaw.yaml
tools:
jira.search:
description: Search Jira issues
parameters:
type: object
properties:
query:
type: string
description: JQL query string
project:
type: string
description: Project key
required:
- query
# 工具执行方式
executor: http
config:
url: https://your-company.atlassian.net/rest/api/3/search
method: POST
headers:
Authorization: Bearer ${JIRA_API_TOKEN}
Content-Type: application/json
body:
jql: "${params.query}"
project: "${params.project}"
方式二:编写 TypeScript 工具
// ~/.openclaw/tools/jira.ts
import { AgentTool, ToolContext, ToolResult } from "@openclaw/tool-types";
export const jiraSearchTool: AgentTool = {
name: "jira.search",
description: "Search Jira issues using JQL",
inputSchema: {
type: "object",
properties: {
query: {
type: "string",
description: "JQL query string, e.g. 'status=Open AND assignee=currentUser()'",
},
maxResults: {
type: "number",
description: "Maximum number of results to return",
default: 10,
},
},
required: ["query"],
},
handler: async (params, ctx: ToolContext): Promise<ToolResult> => {
const { query, maxResults = 10 } = params;
// 1. 获取配置
const jiraConfig = await ctx.getConfig("jira");
if (!jiraConfig) {
return {
content: "Jira is not configured. Please set up Jira integration first.",
isError: true,
};
}
// 2. 调用 Jira API
const response = await fetch(`${jiraConfig.baseUrl}/rest/api/3/search`, {
method: "POST",
headers: {
"Authorization": `Bearer ${jiraConfig.apiToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
jql: query,
maxResults,
fields: ["key", "summary", "status", "priority", "assignee"],
}),
});
if (!response.ok) {
return {
content: `Jira API error: ${response.status} ${response.statusText}`,
isError: true,
};
}
// 3. 格式化结果
const data = await response.json();
const issues = data.issues.map((issue: any) => ({
key: issue.key,
summary: issue.fields.summary,
status: issue.fields.status?.name,
priority: issue.fields.priority?.name,
assignee: issue.fields.assignee?.displayName || "Unassigned",
}));
return {
content: JSON.stringify(issues, null, 2),
};
},
};
注册工具
// ~/.openclaw/tools/index.ts
import { jiraSearchTool } from "./jira";
import { jiraCreateTool } from "./jira-create";
import { jiraCommentTool } from "./jira-comment";
export const tools = [
jiraSearchTool,
jiraCreateTool,
jiraCommentTool,
];
// 在 openclaw.yaml 中引用
// tools:
// import: ~/.openclaw/tools/index.js
工具权限控制
# ~/.openclaw/openclaw.yaml
tools:
permissions:
# 默认策略
default: allow
# 禁止的工具
deny:
- fs.write # 禁止写文件
- code.execute # 禁止执行代码
# 需要确认的工具
confirm:
- sessions.delete # 删除会话需要确认
- config.patch # 修改配置需要确认
# 沙箱模式下的特殊规则
sandbox:
default: deny
allow:
- fs.read
- web.fetch
Hooks:生命周期拦截器
问题:如何在关键时刻介入系统行为?
Hook 允许你在特定事件发生时执行自定义逻辑。
可用的 Hook 点
flowchart TB
subgraph Gateway["Gateway 生命周期"]
H1["gateway:startup<br/>Gateway 启动"]
H2["gateway:shutdown<br/>Gateway 关闭"]
end
subgraph Message["消息生命周期"]
H3["message:received<br/>收到消息"]
H4["message:before_send<br/>发送前"]
H5["message:after_send<br/>发送后"]
end
subgraph Agent["Agent 生命周期"]
H6["agent:before_turn<br/>Turn 开始前"]
H7["agent:tool_call<br/>工具调用时"]
H8["agent:after_turn<br/>Turn 结束后"]
end
subgraph Session["会话生命周期"]
H9["session:created<br/>会话创建"]
H10["session:deleted<br/>会话删除"]
end
实现 Hook
// ~/.openclaw/hooks/content-filter.ts
import { Hook, HookContext, HookResult } from "@openclaw/hook-types";
export const contentFilterHook: Hook = {
name: "content-filter",
// 监听的事件
events: ["message:received", "message:before_send"],
// 执行优先级
priority: 100,
handler: async (ctx: HookContext): Promise<HookResult> => {
const { event, data } = ctx;
if (event === "message:received") {
// 过滤入站消息
const message = data.message as NormalizedMessage;
// 检查是否包含敏感词
const sensitiveWords = ["badword1", "badword2"];
const hasSensitive = sensitiveWords.some(word =>
message.text?.toLowerCase().includes(word)
);
if (hasSensitive) {
// 阻止消息继续处理
return {
action: "reject",
reason: "Message contains sensitive content",
};
}
// 允许继续处理
return { action: "continue" };
}
if (event === "message:before_send") {
// 过滤出站消息
const response = data.response as string;
// 脱敏处理
const sanitized = response.replace(/\b\d{11}\b/g, "[PHONE]");
// 修改消息内容
return {
action: "modify",
data: { ...data, response: sanitized },
};
}
return { action: "continue" };
},
};
配置 Hook
# ~/.openclaw/openclaw.yaml
hooks:
internal:
enabled: true
path: ~/.openclaw/hooks
# 内置 Hook 配置
builtins:
- name: rate-limiter
enabled: true
config:
maxRequests: 100
windowMs: 60000
- name: logger
enabled: true
config:
level: info
format: json
Hook 执行顺序
sequenceDiagram
autonumber
participant Message as 原始消息
participant Hook1 as Hook: content-filter
participant Hook2 as Hook: rate-limiter
participant Core as 核心处理
Message->>Hook1: message:received
Hook1->>Hook1: 检查敏感词
alt 包含敏感词
Hook1-->>Message: reject
else 通过
Hook1->>Hook2: continue
Hook2->>Hook2: 检查频率限制
alt 超出限制
Hook2-->>Message: reject
else 通过
Hook2->>Core: continue
Core-->>Message: 正常处理
end
end
Skills:封装复杂能力
问题:如何复用一套工具和提示词?
Skill 是工具、提示词、配置的组合,封装特定的能力。
Skill 目录结构
~/.openclaw/skills/translator/
├── skill.yaml # Skill 清单
├── prompts/
│ ├── system.md # 系统提示词
│ └── user-template.md # 用户消息模板
├── tools/
│ ├── translate.ts # 翻译工具
│ └── detect-language.ts # 语言检测
└── config/
└── defaults.yaml # 默认配置
Skill 清单
# ~/.openclaw/skills/translator/skill.yaml
id: translator
name: Multi-language Translator
version: 1.0.0
description: Translate text between multiple languages with context awareness
# 入口提示词
prompt: ./prompts/system.md
# 提供的工具
tools:
- ./tools/translate.ts
- ./tools/detect-language.ts
# 配置 Schema
config:
type: object
properties:
defaultSourceLang:
type: string
default: auto
defaultTargetLang:
type: string
default: en
preserveFormatting:
type: boolean
default: true
# 使用示例
examples:
- input: "Translate this to Spanish: Hello, world!"
output: "¡Hola, mundo!"
- input: "What language is 'Bonjour'?"
output: "The text 'Bonjour' is in French."
系统提示词
<!-- ~/.openclaw/skills/translator/prompts/system.md -->
# Translator Skill
You are a professional translator with expertise in multiple languages. Your capabilities include:
1. **Language Detection**: Automatically identify the source language
2. **Context-Aware Translation**: Consider context and cultural nuances
3. **Formatting Preservation**: Maintain original formatting (markdown, HTML, etc.)
4. **Glossary Support**: Use domain-specific terminology
## Workflow
1. When asked to translate:
- Use `detect_language` to identify the source language if not specified
- Use `translate` with appropriate context
- Explain any cultural notes if relevant
2. When asked about a language:
- Provide accurate linguistic information
- Suggest related languages or dialects
## Available Tools
- `translate`: Translate text between languages
- `detect_language`: Identify the language of text
## Configuration
Default source language: {{config.defaultSourceLang}}
Default target language: {{config.defaultTargetLang}}
Preserve formatting: {{config.preserveFormatting}}
使用 Skill
# ~/.openclaw/openclaw.yaml
skills:
enabled:
- translator
# Skill 配置
translator:
defaultTargetLang: zh # 默认翻译成中文
preserveFormatting: true
# 通过 CLI 激活 Skill
openclaw agent chat --skill translator
# 或在对话中引用
# "Use the translator skill to translate this..."
实战演练:开发一个完整的扩展
目标:创建一个 GitHub Issue 查询工具
1. 创建工具
// ~/.openclaw/tools/github-issue.ts
import { AgentTool, ToolContext, ToolResult } from "@openclaw/tool-types";
export const githubGetIssueTool: AgentTool = {
name: "github.get_issue",
description: "Get details of a GitHub issue by number",
inputSchema: {
type: "object",
properties: {
owner: {
type: "string",
description: "Repository owner (e.g., 'openclaw')",
},
repo: {
type: "string",
description: "Repository name (e.g., 'openclaw')",
},
issueNumber: {
type: "number",
description: "Issue number",
},
},
required: ["owner", "repo", "issueNumber"],
},
handler: async (params, ctx: ToolContext): Promise<ToolResult> => {
const { owner, repo, issueNumber } = params;
// 获取 GitHub Token
const token = process.env.GITHUB_TOKEN;
if (!token) {
return {
content: "GITHUB_TOKEN not configured. Set it in your environment or config.",
isError: true,
};
}
try {
const response = await fetch(
`https://api.github.com/repos/${owner}/${repo}/issues/${issueNumber}`,
{
headers: {
"Authorization": `Bearer ${token}`,
"Accept": "application/vnd.github+json",
},
}
);
if (!response.ok) {
if (response.status === 404) {
return { content: `Issue #${issueNumber} not found in ${owner}/${repo}`, isError: true };
}
return { content: `GitHub API error: ${response.status}`, isError: true };
}
const issue = await response.json();
// 格式化输出
const result = {
number: issue.number,
title: issue.title,
state: issue.state,
author: issue.user.login,
created: issue.created_at,
labels: issue.labels.map((l: any) => l.name),
body: issue.body?.substring(0, 500) + (issue.body?.length > 500 ? "..." : ""),
url: issue.html_url,
};
return { content: JSON.stringify(result, null, 2) };
} catch (err) {
return {
content: `Failed to fetch issue: ${err instanceof Error ? err.message : String(err)}`,
isError: true,
};
}
},
};
2. 注册工具
# ~/.openclaw/openclaw.yaml
tools:
import: ~/.openclaw/tools/github-issue.ts
env:
GITHUB_TOKEN: ${GITHUB_TOKEN}
3. 测试工具
# 启动 Gateway
openclaw gateway start
# 测试工具调用
wscat -c "ws://127.0.0.1:18789" -H "Authorization: Bearer $TOKEN"
> {"kind":"request","id":"test","method":"message.send","params":{"sessionKey":"main","text":"What's the latest issue in openclaw/openclaw repo?"}}
< {"kind":"event","event":"agent","payload":{"type":"tool_call","name":"github.get_issue","args":{"owner":"openclaw","repo":"openclaw","issueNumber":1}}}
< {"kind":"event","event":"agent","payload":{"type":"tool_result","result":"{...}"}}
< {"kind":"event","event":"agent","payload":{"type":"text_delta","text":"The issue #1 in openclaw/openclaw is titled..."}}
常见陷阱
陷阱 1:插件加载失败
症状:
[ERROR] Failed to load plugin: signal
[ERROR] Error: Cannot find module '@openclaw/channel-types'
原因: 插件依赖未安装或模块路径不正确。
错误代码:
# 插件配置正确但依赖未安装
# ~/.openclaw/plugins/signal/openclaw.plugin.yaml
dependencies:
- signal-cli # 声明了但未安装
正确代码:
# 安装插件依赖
cd ~/.openclaw/plugins/signal
npm install @openclaw/channel-types
# 或使用 openclaw 命令自动安装
openclaw plugin install signal
# 验证插件加载
openclaw plugin list
# 输出
# signal (v1.0.0) - Signal messaging channel support [ENABLED]
调试方法:
# 查看插件加载日志
LOG_LEVEL=debug LOG_INCLUDE="plugins,loader" openclaw gateway start
# 输出示例
[DEBUG] plugins: Loading plugin from ~/.openclaw/plugins/signal
[DEBUG] plugins: Resolving dependencies: @openclaw/channel-types, signal-cli
[ERROR] plugins: Failed to resolve dependency: @openclaw/channel-types
[DEBUG] plugins: Plugin load failed: signal
源码位置: src/plugins/loader.ts
陷阱 2:Hook 执行顺序错误
症状:
[WARN] Hook 'rate-limiter' executed after 'content-filter'
[WARN] Content filter bypassed due to execution order
原因: Hook 的优先级设置不当,导致执行顺序与预期不符。
错误代码:
// rate-limiter 优先级低于 content-filter
// 但实际上应该在 content-filter 之前执行
export const rateLimiterHook: Hook = {
name: "rate-limiter",
events: ["message:received"],
priority: 50, // 较低优先级,后执行
handler: async (ctx) => { ... },
};
export const contentFilterHook: Hook = {
name: "content-filter",
events: ["message:received"],
priority: 100, // 较高优先级,先执行
handler: async (ctx) => { ... },
};
正确代码:
// 优先级越高,越先执行
// rate-limiter 应该最先执行(阻止恶意请求)
export const rateLimiterHook: Hook = {
name: "rate-limiter",
events: ["message:received"],
priority: 1000, // 最高优先级
handler: async (ctx) => {
// 先检查频率限制
const allowed = await checkRateLimit(ctx.sessionKey);
if (!allowed) {
return { action: "reject", reason: "Rate limit exceeded" };
}
return { action: "continue" };
},
};
// content-filter 其次执行
export const contentFilterHook: Hook = {
name: "content-filter",
events: ["message:received"],
priority: 500, // 中等优先级
handler: async (ctx) => { ... },
};
// logger 最后执行(记录已过滤的消息)
export const loggerHook: Hook = {
name: "logger",
events: ["message:received"],
priority: 100, // 较低优先级
handler: async (ctx) => { ... },
};
调试方法:
# 查看 Hook 执行顺序
LOG_LEVEL=debug LOG_INCLUDE="hooks" openclaw gateway start
# 输出示例
[DEBUG] hooks: Registering hooks for event: message:received
[DEBUG] hooks: - rate-limiter (priority: 1000)
[DEBUG] hooks: - content-filter (priority: 500)
[DEBUG] hooks: - logger (priority: 100)
[DEBUG] hooks: Executing hooks in order...
[DEBUG] hooks: Executing rate-limiter...
[DEBUG] hooks: rate-limiter returned: continue
[DEBUG] hooks: Executing content-filter...
[DEBUG] hooks: content-filter returned: continue
[DEBUG] hooks: Executing logger...
源码位置: src/hooks/manager.ts
陷阱 3:工具注册重复
症状:
[ERROR] Tool jira.search already registered
[ERROR] Failed to register tool from ~/.openclaw/tools/custom-jira.ts
原因: 同一个工具被多次注册,可能是配置文件和代码都定义了同名工具。
错误代码:
# ~/.openclaw/openclaw.yaml
tools:
# 通过配置定义
jira.search:
description: Search Jira issues
# ...
# 又通过 import 导入了同名工具
import: ~/.openclaw/tools/jira.ts # 里面也定义了 jira.search
正确代码:
# ~/.openclaw/openclaw.yaml
tools:
# 方式一:只用配置定义(简单场景)
jira.search:
description: Search Jira issues
# ...
# 方式二:只用 import(复杂场景)
# import: ~/.openclaw/tools/jira.ts
# 如果两者都需要,使用不同的名称
jira.search.v2:
description: Search Jira issues (v2)
# ...
调试方法:
# 查看已注册的工具
openclaw tools list
# 输出示例
# Tool Name | Description
# -----------------------|----------------------------------
# sessions.list | List all available sessions
# sessions.send | Send a message to a session
# jira.search | Search Jira issues
# fs.read | Read file contents
# ...
# 查看工具来源
openclaw tools list --verbose
# 输出示例
# jira.search (from config: ~/.openclaw/openclaw.yaml)
# sessions.list (from builtin: src/agents/tools/builtin.ts)
源码位置: src/agents/tools/registry.ts
陷阱 4:Skill 配置不生效
症状:
[WARN] Skill 'translator' loaded but defaultTargetLang is ignored
[WARN] Agent still uses 'en' as target language
原因: Skill 配置未正确传递,或配置 Schema 与实际配置不匹配。
错误代码:
# 配置路径错误
skills:
translator:
targetLang: zh # 应该是 defaultTargetLang
正确代码:
# ~/.openclaw/openclaw.yaml
skills:
enabled:
- translator
# 配置必须与 skill.yaml 中的 config Schema 匹配
translator:
defaultSourceLang: auto
defaultTargetLang: zh # 正确的字段名
preserveFormatting: true
调试方法:
# 验证 Skill 配置
openclaw skill validate translator
# 输出示例
# Validating skill: translator
# ✓ skill.yaml is valid
# ✓ prompts/system.md exists
# ✓ tools/translate.ts exists
# ✗ Config validation failed:
# - Unknown property 'targetLang' at skills.translator
# - Did you mean 'defaultTargetLang'?
# 查看有效配置
openclaw skill config translator
# 输出示例
# translator config:
# defaultSourceLang: auto
# defaultTargetLang: zh
# preserveFormatting: true
源码位置: src/skills/loader.ts
设计启示:扩展系统的权衡
优势
| 设计 | 好处 |
|---|---|
| 插件隔离 | 核心稳定,扩展独立演进 |
| 工具 Schema | LLM 能理解工具能力 |
| Hook 模式 | 无侵入式扩展 |
| Skill 封装 | 能力复用,降低使用门槛 |
劣势
| 问题 | 原因 |
|---|---|
| 性能开销 | 插件加载和通信开销 |
| 调试复杂 | 跨模块问题难追踪 |
| 版本兼容 | 插件 API 变更需要协调 |
最佳实践
-
工具设计
- 单一职责,每个工具只做一件事
- 输入验证,防止无效参数
- 错误友好,返回可理解的错误信息
-
Hook 设计
- 快速执行,避免阻塞主流程
- 幂等操作,支持重试
- 优雅降级,Hook 失败不应导致系统崩溃
-
插件开发
- 声明依赖,方便安装检查
- 提供配置 Schema,支持校验
- 编写测试,确保质量
小结
OpenClaw 的扩展机制让你可以:
| 扩展点 | 做什么 | 怎么做 |
|---|---|---|
| Channel Plugin | 接入新平台 | 实现 ChannelPlugin 接口 |
| Custom Tool | 扩展 Agent 能力 | 定义工具 Schema 和 Handler |
| Hook | 介入生命周期 | 监听事件并返回结果 |
| Skill | 封装复杂能力 | 组合提示词、工具、配置 |
理解扩展点的关键:每个扩展点解决一个特定问题,通过标准接口与核心解耦。
关键源码文件
| 文件 | 职责 |
|---|---|
src/plugins/loader.ts |
插件加载器 |
src/plugins/registry.ts |
插件注册表 |
src/agents/tools/registry.ts |
工具注册表 |
src/hooks/manager.ts |
Hook 管理器 |
extensions/ |
内置插件目录 |
skills/ |
内置技能目录 |
在下一篇文章中,我们将进入 部署与安全——如何在生产环境中安全地运行 OpenClaw。
系列索引: OpenClaw 源码解析:目录索引
上一篇: OpenClaw 源码解析(三):Agent——AI 的躯壳与灵魂 下一篇: OpenClaw 源码解析(五):部署与安全