1. 项目概述:从零构建你自己的AI助手框架

最近在AI应用开发领域,一个名为OpenClaw的项目引起了我的注意。它是一个开源的、多通道的AI助手框架,允许开发者构建能够通过终端、飞书、Telegram等多种平台与用户交互的智能体。然而,其庞大的代码库(超过90万行)对于想要深入理解其核心架构或进行二次开发的开发者来说,门槛相当高。这正是“Build Your Own OpenClaw”这个教程项目的价值所在——它用大约2600行TypeScript代码,精炼地复现了OpenClaw最核心的设计模式和架构思想。

这个项目本质上是一个教学性质的“极简复刻版”,我称之为MyClaw。它剥离了生产环境所需的大量边缘功能、性能优化和平台特定代码,只保留了最精髓的部分:一个基于LLM的智能体运行时、一个可扩展的多通道消息路由系统、一个简单的插件与技能机制,以及一个用于连接外部服务的网关服务器。通过亲手实现这2600行代码,你不仅能获得一个可以立即运行、支持基础对话的AI助手,更能透彻理解现代AI应用后端是如何处理会话、路由消息、集成不同模型以及管理扩展功能的。这对于任何希望构建自己的AI Agent或集成AI能力到现有产品的开发者来说,都是一次绝佳的、低成本的架构学习之旅。

2. 核心架构与设计思路拆解

在开始动手写代码之前,理解MyClaw的整体设计思路至关重要。它采用了清晰的分层和模块化设计,每一层都有明确的职责,这使得系统既易于理解也便于扩展。

2.1 分层架构解析

MyClaw的架构可以抽象为四个核心层次,数据流自下而上或横向流动:

  1. 通道层 (Channels) :这是系统与外部世界交互的边界。每个通道(如终端、飞书、Telegram)都是一个独立的模块,负责监听特定平台的消息,并将平台特定的消息格式转换为系统内部统一的协议格式,反之亦然。通道层实现了“多入口”,是系统扩展性的基础。

  2. 路由层 (Routing) :这是系统的“交通枢纽”。它接收来自所有通道的标准化消息,并根据预定义的规则(如关键词匹配、@提及、默认路由)决定将消息分发给哪个或哪些智能体(Agent)进行处理。在MyClaw的简化版本中,通常只有一个主Agent,但路由层为未来的多Agent协作预留了架构可能性。

  3. 智能体层 (Agent Runtime) :这是系统的“大脑”。它封装了与大型语言模型(LLM)的交互逻辑。其核心是一个运行循环(Agent Loop),负责维护会话上下文(记忆),将路由层转发的用户消息与历史记录组合成提示词(Prompt),调用LLM API获取响应,并可能触发技能(Skills)的执行。最后,它将LLM的响应或技能的执行结果返回给路由层。

  4. 网关与配置层 (Gateway & Configuration) :这是系统的“控制中心”和“后勤部门”。网关作为一个WebSocket服务器,为需要长连接的外部通道(如需要实时推送的客户端)提供接入点。配置层则管理着所有运行参数,从LLM的API密钥、模型选择到各个通道的启用状态,确保系统行为是可配置的。

2.2 关键设计模式与选型理由

为什么选择这样的架构和这些技术栈?背后有非常实际的工程考量:

  • 依赖注入与控制反转(IoC) :在CLI框架和插件系统中大量使用。这允许我们在运行时动态地组装和替换组件(如不同的通道、不同的LLM模型),而不是在代码中硬编码依赖关系。这使得单元测试变得容易(可以注入Mock对象),也提升了代码的模块化和可维护性。
  • 事件驱动与观察者模式 :通道基类( Transport )继承自Node.js的 EventEmitter 。当通道收到消息时,它发射一个 ‘message’ 事件;路由层监听这个事件并进行处理。这种松耦合的设计意味着新增一个通道时,你只需要确保它能正确发射事件,而无需修改路由器的核心代码。
  • 策略模式 :在处理不同LLM提供商(OpenAI, Anthropic, OpenRouter)时,虽然它们有相似的API(发送消息,获取流式响应),但请求体格式、头部认证和响应解析各有不同。MyClaw通过一个统一的模型接口( Model )和具体的提供商实现类,将变化的算法封装起来,使得在运行时切换模型提供商变得非常简单。
  • 使用TypeScript和Zod :TypeScript提供了静态类型检查,能在编码阶段捕获大量潜在错误,这对于构建有复杂数据流(如消息协议)的系统至关重要。Zod库则用于运行时数据的验证和解析,它与TypeScript的类型系统能完美协作,确保从配置文件、环境变量或网络传入的数据格式是正确且安全的,避免了“垃圾进,垃圾出”的问题。
  • 选择Commander.js作为CLI框架 :对于需要复杂子命令和参数解析的CLI工具,Commander.js是Node.js生态的事实标准。它极大地简化了命令行参数的定义、解析和帮助文档的生成,让我们能专注于业务逻辑而非命令行处理。

设计心得 :在构建此类中间件或框架时,一个核心原则是“面向接口编程,而非实现编程”。无论是通道、模型还是技能,都先定义好它们必须遵守的契约(接口),然后再去编写具体实现。这为未来的替换和扩展打下了坚实的基础,也是MyClaw代码虽少但结构清晰的关键。

3. 核心模块深度解析与实操要点

接下来,我们深入到几个最关键模块的内部,看看它们是如何工作的,以及在实现时需要注意哪些“坑”。

3.1 智能体运行时:与LLM对话的核心引擎

src/agent/runtime.ts 文件是MyClaw的“心脏”。它定义了 AgentRuntime 类,负责管理整个与AI对话的生命周期。

核心流程(Agent Loop) :

  1. 初始化会话 :为每个新的对话会话创建一个唯一的ID,并初始化一个空的消息历史数组。
  2. 接收用户输入 :从路由层接收用户消息,将其转换为系统内部的消息格式(通常包含角色 user 、内容和会话ID)。
  3. 构建上下文 :从持久化存储(在MyClaw简化版中可能是内存,复杂版会用数据库)中加载该会话的历史消息,将新消息追加到历史中。这里涉及一个关键优化:上下文窗口管理。LLM的上下文长度是有限的(如Claude 200K,GPT-4 128K),不能无限制地堆积历史。常见的策略是保留最近N条消息,或当总token数接近上限时,丢弃最老的几条消息,但可能保留系统提示词和关键摘要。
  4. 调用LLM :将构建好的消息历史(可能加上系统提示词)发送给选定的LLM提供商API。这里支持 流式响应 ,即边生成边返回,而不是等待全部生成完毕。这对于终端或聊天应用的用户体验至关重要。
  5. 处理工具调用(Skills) :如果LLM的响应中包含了对某个工具(Skill)的调用请求(通常以特定JSON格式),运行时需要解析这个请求,找到对应的技能函数并执行它,然后将执行结果作为新的消息追加到历史中,再次调用LLM,形成“思考-行动-观察”的循环。这是实现AI使用外部能力的关键。
  6. 返回响应 :将LLM生成的最终文本响应(或流式响应的最终结果)返回给路由层,进而通过对应通道返回给用户。

实操要点与避坑指南

  • API密钥与模型配置 :密钥务必通过环境变量或配置文件传入,绝不要硬编码在代码中。 src/agent/model.ts 中的 resolveModel 函数负责根据配置选择正确的模型提供商和构造请求参数。注意不同提供商的速率限制和计费方式,在代码中可以考虑加入简单的请求队列或退避重试逻辑。
  • 错误处理 :LLM API调用可能因网络、超时、额度不足等原因失败。必须用 try-catch 包裹,并提供友好的错误信息,同时考虑重试机制(对于临时性网络错误)。
  • 流式处理 :处理流式响应时,要正确处理 data 事件,并注意响应可能被分成多个 chunk 。需要实现一个简单的解析器来累积这些chunk,并从中提取出有效的文本增量。同时,要为客户端(如终端)设计一个良好的进度显示。

3.2 消息路由:智能的消息分发器

src/routing/router.ts 中的 Router 类是系统的“中枢神经”。它监听所有通道的消息事件,并决定消息的去向。

路由策略 : 在MyClaw中,路由逻辑相对直接,但设计上是可扩展的。一个典型的路由逻辑可能包括:

  1. 精确匹配 :检查消息是否以特定的命令或技能名开头(如 /translate )。
  2. 提及匹配 :检查消息中是否@了某个特定的机器人名称(在群聊中尤其有用)。
  3. 默认路由 :如果以上都不匹配,则将消息路由到默认的AI助手进行处理。

实现关键 : 路由器本身也继承自 EventEmitter 。当它决定好消息的目标后,会发射一个 ‘agent_request’ 事件。 AgentRuntime 则监听这个事件,开始处理。这种设计将路由决策与业务处理完全解耦。

经验之谈 :在实际项目中,路由规则会复杂得多。你可能需要基于用户身份、群组、上下文、甚至消息内容的情感分析来路由。因此,在 Router 类中,应将路由规则设计成可配置的、甚至可插拔的模块。MyClaw提供了一个基础的框架,你可以很容易地扩展 routeMessage 方法,加入更复杂的规则引擎。

3.3 通道抽象:统一的多平台接入层

src/channels/transport.ts 定义的 Transport 抽象类是通道系统的基石。所有具体通道(如 TerminalChannel , FeishuChannel )都必须继承并实现这个接口。

抽象类定义的核心方法

  • start() : 启动通道监听(如启动WebSocket服务器、开始轮询等)。
  • stop() : 优雅地停止通道。
  • send(message) : 向该通道连接的客户端发送消息。
  • receive(message) : (通常由子类在收到平台消息时调用)将平台原生消息转换为内部格式,并触发 ‘message’ 事件。

以Telegram通道为例 ( src/channels/telegram.ts ):

  1. 初始化 :使用从环境变量获取的 TELEGRAM_BOT_TOKEN 初始化 grammY 机器人框架。
  2. 消息监听 :设置中间件( bot.on(‘message’) )来监听所有消息。
  3. 格式转换 :在中间件中,将 grammY Context 对象中的复杂消息(可能包含文本、图片、命令等)提取并转换成MyClaw内部统一的 IncomingMessage 格式。这个过程需要处理 Message 对象的多种类型。
  4. 事件触发 :转换完成后,调用 this.receive(internalMessage) ,从而触发父类 Transport ‘message’ 事件,让路由器捕获。
  5. 消息发送 :当需要回复时, AgentRuntime 通过路由器最终调用通道的 send 方法。 send 方法需要将内部消息格式再转换回Telegram Bot API所需的格式,并调用 bot.api.sendMessage

注意事项

  • 异步处理 :所有网络I/O都是异步的。务必妥善处理 async/await ,避免未处理的Promise拒绝。
  • 错误边界 :每个通道应该有自己的错误处理逻辑,捕获平台API的异常,防止一个通道的崩溃影响整个系统。
  • 速率限制 :像Telegram、飞书等平台对机器人API都有调用频率限制。在 send 方法中应考虑加入简单的限流或队列机制,避免触发平台限制。

3.4 技能系统:赋予AI“动手”能力

技能(Skills)是扩展AI助手能力的核心机制。它允许AI在思考后,决定调用一个外部函数来获取信息或执行操作。

技能的定义与加载 : 在MyClaw中,一个技能是一个独立的文件夹(如 skills/translate/ ),其中必须包含一个 SKILL.md 文件。这个Markdown文件非常关键,它采用特定格式:

# skill_name
A description of what this skill does.

## prompt
The system prompt that will be injected when this skill is invoked. This teaches the LLM when and how to use this skill.

## command
/translate

## function
```typescript
// TypeScript code defining the function signature and implementation
export async function translate(params: { text: string; targetLang: string }): Promise<string> {
  // ...调用真实翻译API的逻辑...
}
`src/skills/loader.ts` 会扫描`skills`目录,解析每个`SKILL.md`文件,提取出技能名称、描述、提示词、触发命令和函数实现,并将其注册到`SkillRegistry`中。

**技能的执行流程**:
1.  **技能提示词注入**:当路由器识别出消息以技能命令(如`/translate`)开头时,或者LLM在思考过程中决定使用技能时,该技能对应的`## prompt`部分会被动态添加到本次请求的系统提示词中,指导LLM如何格式化它的工具调用请求。
2.  **LLM生成工具调用**:LLM在理解了用户意图和技能描述后,会在响应中返回一个结构化的工具调用请求(遵循OpenAI的`function_call`或Anthropic的`tool_use`格式)。
3.  **运行时执行工具**:`AgentRuntime`拦截到这个请求,解析出要调用的技能名和参数。
4.  **查找并执行**:从`SkillRegistry`中找到对应的函数,使用解析出的参数执行它。这是一个真实的异步函数调用,可以访问网络、数据库、文件系统等。
5.  **结果反馈与继续**:将函数执行的结果(成功或失败)作为一条新的`system`或`tool`角色消息,追加到对话历史中,然后再次调用LLM,让AI根据工具执行结果来生成面向用户的最终回答。

**设计精髓**:
这种设计巧妙地将“技能描述”和“技能实现”绑定在一起,并且通过动态提示词注入,让LLM在运行时学习如何使用技能。它比硬编码的`if-else`命令处理方式要强大和灵活得多,是构建真正“智能”助手而非“脚本”助手的关键。

## 4. 从零开始的完整实操指南

理解了核心概念后,让我们一步步从零搭建并运行起自己的MyClaw。我将以在Linux/macOS开发环境为例,Windows用户可能需要稍作调整(主要是环境变量设置和路径)。

### 4.1 环境准备与项目初始化

首先,确保你的系统满足基础要求:
*   **Node.js**:版本必须在20或以上。这是很多现代ES模块和API的要求。你可以使用`nvm`(Node Version Manager)来轻松安装和管理多个Node版本。
    ```bash
    # 检查Node版本
    node --version
    # 如果版本低于20,使用nvm安装
    nvm install 20
    nvm use 20
    ```
*   **代码编辑器**:推荐VS Code,并安装TypeScript和ESLint插件以获得最佳开发体验。
*   **Git**:用于克隆代码库。

接下来,获取项目代码并安装依赖:
```bash
# 克隆教程仓库
git clone https://github.com/anthropics/build-your-own-openclaw.git
cd build-your-own-openclaw

# 安装项目依赖
npm install

执行 npm install 后,你会看到项目安装了以下关键依赖:

  • commander : 构建CLI工具。
  • zod : 运行时数据验证。
  • ws : WebSocket服务器实现。
  • grammy : Telegram Bot框架。
  • openai , @anthropic-ai/sdk : 官方LLM SDK。
  • typescript , tsx , @types/node : 开发工具链。

4.2 核心配置详解与初始化

MyClaw的配置系统是其灵活性的体现。它支持通过交互式向导、YAML配置文件和环境变量三种方式配置。

运行配置向导 : 这是最推荐的方式,它会引导你完成所有必要设置。

npx myclaw onboard

这个命令会启动一个交互式命令行问卷,依次询问:

  1. LLM Provider : 选择主要的AI模型提供商(OpenRouter(默认,有免费模型)、Anthropic、OpenAI)。
  2. API Key : 输入对应提供商的API密钥。向导会验证密钥的有效性。
  3. Model Name : 选择要使用的具体模型(如 claude-3-haiku-20240307 , gpt-4o-mini )。列表会根据你选择的提供商动态变化。
  4. Gateway Port : 网关服务器监听的端口号(默认 3000 )。
  5. Bot Name : 你的AI助手名字,用于在聊天中@提及。
  6. 启用飞书/Telegram通道 : 询问你是否要现在配置这些外部通道。

问卷完成后,它会在你的用户主目录下创建 ~/.myclaw/myclaw.yaml 配置文件。 强烈建议你打开这个文件查看其结构 ,它清晰地定义了所有可配置项。

配置文件解析 ( ~/.myclaw/myclaw.yaml )

# 这是生成的配置文件示例
llm:
  provider: 'openrouter' # 或 'anthropic', 'openai'
  apiKey: 'sk-xx...' # 实际密钥已安全存储
  model: 'google/gemma-2-9b-it:free'
  # 其他提供商特定参数,如baseUrl(用于自定义代理或本地模型)

gateway:
  port: 3000
  host: 'localhost'

bot:
  name: 'MyAssistant'

channels:
  feishu:
    enabled: false
    # appId 和 appSecret 应通过环境变量设置
  telegram:
    enabled: false
    # botToken 应通过环境变量设置

skills:
  # 启用的技能列表
  enabled:
    - 'translate'
    - 'summarize'

环境变量优先 :配置文件中的敏感信息(如 apiKey )在生成时会被保存,但对于飞书的 FEISHU_APP_ID FEISHU_APP_SECRET 和Telegram的 TELEGRAM_BOT_TOKEN ,教程更推荐通过环境变量设置,因为这样更安全,也便于在容器化部署时管理。

4.3 启动与基础功能测试

配置完成后,你可以开始体验MyClaw的核心功能。

1. 终端直接对话模式 : 这是最简单的测试方式,不启动网关,直接在终端与AI交互。

npx myclaw agent

运行后,你会进入一个交互式REPL界面。输入你的问题,比如“Hello, who are you?”,AI会进行回复。你可以使用 /help 查看内置命令(如退出)。这个模式直接使用了 AgentRuntime ,是验证LLM连接和基础对话能力最快的方式。

2. 启动网关服务器 : 网关模式会启动WebSocket服务器,并激活所有在配置中启用的通道。

npx myclaw gateway

如果一切正常,终端会输出类似 Gateway server listening on ws://localhost:3000 的信息。此时,网关正在运行,但如果没有启用任何外部通道(飞书、Telegram),它可能只是在等待连接。你可以使用 curl 或WebSocket客户端工具测试网关端口是否开放。

3. 运行系统诊断 : 这是一个非常有用的工具,用于检查系统状态和配置问题。

npx myclaw doctor

doctor 命令会执行一系列检查:Node版本、配置文件是否存在且有效、API密钥连通性、必要端口是否被占用等。任何问题都会以清晰的方式报告出来,是排查启动故障的首选命令。

4.4 集成外部通道实战

让AI助手在终端里对话只是第一步,集成到日常使用的聊天工具中才是其价值所在。

集成Telegram Bot

  1. 创建Bot :在Telegram中搜索 @BotFather ,发送 /newbot 命令,按照提示设置名字和用户名。创建成功后, BotFather 会给你一个 HTTP API token ,形如 1234567890:ABCdefGhIJKlmNoPQRsTUVwxyZ
  2. 配置环境变量 :将Token设置为环境变量。
    export TELEGRAM_BOT_TOKEN='你的BotToken'
    # 为了使环境变量永久生效,可以将其添加到 ~/.bashrc 或 ~/.zshrc 文件中
    
  3. 启用并启动 :确保你的 myclaw.yaml 配置文件中 channels.telegram.enabled true (运行 onboard 向导时可设置)。然后启动网关: npx myclaw gateway
  4. 与Bot互动 :在Telegram中找到你的Bot,发送 /start 或任何消息。MyClaw网关会接收到消息,通过AI处理后再回复给你。

关键细节 :Telegram Bot默认使用 长轮询 来获取更新。 grammY 库会帮你处理这个过程。这意味着你的网关服务器需要能对外访问(有公网IP或使用内网穿透),Telegram的服务器才能将消息推送到你的 /webhook 端点。在开发时,可以使用 ngrok localtunnel 等工具将本地端口暴露到公网,并将生成的HTTPS URL设置为Bot的Webhook地址。不过,MyClaw教程的简化版本可能默认使用了长轮询,这要求你的服务器能主动访问Telegram API,对于有防火墙限制的环境可能需要配置。

集成飞书/Lark机器人 : 飞书的集成相对复杂,因为它涉及飞书开放平台的配置。

  1. 创建企业自建应用 :登录 飞书开放平台 ,创建一个“企业自建应用”。
  2. 获取凭证 :在应用详情页,找到 App ID App Secret
  3. 配置环境变量
    export FEISHU_APP_ID='your_app_id'
    export FEISHU_APP_SECRET='your_app_secret'
    
  4. 启用功能 :在开放平台后台,为你的应用启用“机器人”能力。
  5. 配置事件订阅 :这是最关键的一步。你需要提供一个服务器URL(你的MyClaw网关地址,需公网可访问)来接收飞书的事件。飞书会向这个URL发送验证请求(包含一个 challenge 参数),你的服务器必须原样返回这个 challenge 值才能通过验证。 src/channels/feishu.ts 中的代码已经处理了这个验证逻辑。
  6. 发布应用 :在开发环境,你可以将应用发布到“测试企业”,然后邀请你的测试账号进入有该机器人的群组或直接与机器人私聊。

启动网关后,你就可以在飞书中@你的机器人进行对话了。飞书通道使用的是WebSocket连接(对于事件监听),实时性更好。

5. 开发、调试与扩展指南

当你成功运行起基础版本后,很可能会萌生修改和扩展它的想法。以下是基于我个人经验的开发工作流和扩展思路。

5.1 本地开发与调试技巧

使用开发模式运行 : 项目 package.json 中已经定义好了开发脚本。

# 使用tsx直接运行TypeScript源码,无需编译
npm run dev -- agent
# 或者运行网关
npm run dev -- gateway

tsx 是一个高性能的TypeScript运行时,它会在内存中即时编译并执行 .ts 文件,极大地加快了开发时的修改-测试循环。

调试Node.js应用 : 在VS Code中,可以轻松配置调试。

  1. 在项目根目录创建或编辑 .vscode/launch.json
  2. 添加一个配置:
    {
      "version": "0.2.0",
      "configurations": [
        {
          "type": "node",
          "request": "launch",
          "name": "Debug MyClaw Agent",
          "skipFiles": ["<node_internals>/**"],
          "program": "${workspaceFolder}/src/entry.ts",
          "args": ["agent"],
          "runtimeArgs": ["--loader", "tsx"]
        }
      ]
    }
    
  3. 在代码中打上断点,然后按F5启动调试。你可以观察变量、调用栈,单步执行,这对于理解消息流和排查逻辑错误非常有用。

日志记录 : MyClaw教程项目可能使用了简单的 console.log 。对于更正式的项目,建议集成一个结构化的日志库,如 winston pino ,可以按级别(DEBUG, INFO, WARN, ERROR)输出日志,并方便地输出到文件或日志服务,这对于追踪生产环境的问题至关重要。

5.2 如何添加一个新的技能

添加技能是扩展AI能力最直接的方式。假设我们要添加一个“查询天气”的技能。

  1. 创建技能目录和文件

    mkdir -p skills/weather
    touch skills/weather/SKILL.md
    
  2. 编写SKILL.md

    # weather
    Get the current weather for a specified city.
    
    ## prompt
    When the user asks about the weather or temperature in a city, use the `weather` skill. The skill requires a `city` parameter which is the name of the city (e.g., "Beijing", "New York"). Respond with a friendly summary of the weather information.
    
    ## command
    /weather
    
    ## function
    ```typescript
    export interface WeatherParams {
      city: string;
    }
    
    export async function weather(params: WeatherParams): Promise<string> {
      // 注意:这是一个模拟函数。真实场景下,你需要调用如OpenWeatherMap的API。
      // 你需要申请一个API key (https://openweathermap.org/api)
      const apiKey = process.env.OPENWEATHER_API_KEY;
      if (!apiKey) {
        return 'Weather service is not configured. Please set OPENWEATHER_API_KEY.';
      }
    
      try {
        // 示例API调用(实际URL和参数请查阅文档)
        const response = await fetch(
          `https://api.openweathermap.org/data/2.5/weather?q=${encodeURIComponent(params.city)}&appid=${apiKey}&units=metric`
        );
        const data = await response.json();
        if (data.cod !== 200) {
          return `Could not find weather for city: ${params.city}.`;
        }
        const temp = data.main.temp;
        const desc = data.weather[0].description;
        return `The current weather in ${params.city} is ${desc} with a temperature of ${temp}°C.`;
      } catch (error) {
        console.error('Weather API error:', error);
        return 'Sorry, I encountered an error while fetching the weather.';
      }
    }
    
    
    
  3. 更新配置 :在 myclaw.yaml 文件的 skills.enabled 列表中添加 - ‘weather’

  4. 设置API密钥 :将真实的 OPENWEATHER_API_KEY 添加到环境变量中。

  5. 重启网关或Agent :现在你可以问AI“What‘s the weather like in Shanghai?”或者直接发送 /weather Shanghai 命令,AI就会调用你的新技能了。

技能设计要点

  • 清晰的描述和提示词 ## prompt 部分是给AI看的“说明书”,必须清晰、无歧义地说明技能的用途、调用时机和参数格式。
  • 健壮的错误处理 :技能函数必须考虑网络超时、API错误、无效输入等情况,并返回友好的错误信息,让AI能够生成得体的用户回复。
  • 类型安全 :使用TypeScript接口(如 WeatherParams )定义参数,这能在编译时和运行时(通过Zod验证)提供保障。

5.3 如何添加一个新的通道

添加一个新平台的支持,是扩大助手覆盖面的关键。假设我们要添加一个Discord频道支持。

  1. 安装依赖 :首先需要Discord的Node.js SDK。

    npm install discord.js
    
  2. 创建通道文件 :在 src/channels/ 目录下创建 discord.ts

    import { Client, GatewayIntentBits, Message } from 'discord.js';
    import { Transport, IncomingMessage } from './transport.js';
    
    export class DiscordChannel extends Transport {
      private client: Client;
      private token: string;
    
      constructor(config: { token: string }) {
        super('discord');
        this.token = config.token;
        this.client = new Client({
          intents: [
            GatewayIntentBits.Guilds,
            GatewayIntentBits.GuildMessages,
            GatewayIntentBits.MessageContent, // 需要此权限来读取消息内容
          ],
        });
        this.setupListeners();
      }
    
      private setupListeners(): void {
        this.client.on('ready', () => {
          console.log(`Discord bot logged in as ${this.client.user?.tag}`);
        });
    
        this.client.on('messageCreate', async (message: Message) => {
          // 防止机器人响应自己的消息
          if (message.author.bot) return;
    
          // 将Discord消息转换为内部格式
          const internalMessage: IncomingMessage = {
            id: message.id,
            channelId: message.channel.id,
            userId: message.author.id,
            username: message.author.username,
            text: message.content,
            platform: 'discord',
            raw: message, // 保留原始对象,以备后用
          };
    
          // 触发路由事件
          this.receive(internalMessage);
        });
      }
    
      async start(): Promise<void> {
        await this.client.login(this.token);
        console.log('Discord channel started.');
      }
    
      async stop(): Promise<void> {
        this.client.destroy();
        console.log('Discord channel stopped.');
      }
    
      async send(message: any): Promise<void> {
        // 这里的message是路由器传过来的,包含channelId和text
        const { channelId, text } = message;
        const channel = await this.client.channels.fetch(channelId);
        if (channel?.isTextBased()) {
          await channel.send(text);
        }
      }
    }
    
  3. 注册通道 :需要在系统启动时(例如在 src/entry.ts 或通道管理器中)实例化 DiscordChannel ,并传入从环境变量 DISCORD_BOT_TOKEN 获取的配置。

  4. 配置与启动 :将Bot Token添加到环境变量,并在配置中启用Discord通道,重启网关即可。

通道开发核心

  • 消息格式转换 receive 方法负责将平台原生消息对象转换为统一的 IncomingMessage
  • 消息发送 send 方法负责将内部消息格式转换回平台所需的格式并发送。
  • 平台特性处理 :不同平台有不同特性(如Discord的Slash Commands、消息组件)。高级实现中,你需要在转换时处理这些特性,或在 send 时支持富媒体回复。

5.4 性能优化与生产部署考量

教程项目侧重于教学,在生产环境部署前,有几个方面需要加强:

  • 会话持久化 :当前会话可能存储在内存中,服务器重启后历史记录会丢失。需要集成数据库(如SQLite、PostgreSQL、Redis)来持久化存储会话和消息历史。
  • 配置管理 :将敏感信息(API密钥)移出配置文件,使用专业的密钥管理服务(如AWS Secrets Manager, HashiCorp Vault)或至少使用环境变量。
  • 可观测性 :添加更完善的日志、指标(Metrics)和分布式追踪(Tracing),以便监控机器人的健康状况、性能和使用情况。
  • 速率限制与队列 :为LLM API调用和对外通道的消息发送实现速率限制和队列,防止因突发流量触发平台限制或产生过高费用。
  • 容器化 :使用Docker将应用容器化,可以简化依赖管理和部署流程。编写 Dockerfile docker-compose.yml 是标准做法。
  • 进程管理 :在生产环境,不应直接使用 node 命令运行。应使用进程管理器如 PM2 systemd 来保证应用崩溃后自动重启,并管理日志轮转。

6. 常见问题排查与解决实录

在开发和运行过程中,你几乎一定会遇到下面这些问题。这里是我踩过坑后总结的排查清单。

6.1 启动与连接问题

问题现象 可能原因 排查步骤与解决方案
运行 npx myclaw agent 立即报错或退出 1. Node.js版本过低。
2. 配置文件缺失或格式错误。
3. 关键依赖未安装。
1. 运行 node --version 确认版本≥20。
2. 运行 npx myclaw doctor 检查配置。
3. 删除 node_modules package-lock.json ,重新运行 npm install
doctor 命令显示API密钥无效 1. 密钥输入错误。
2. 密钥未正确设置到环境变量或配置文件。
3. 提供的API密钥没有对应所选模型的权限。
1. 仔细核对密钥,确保没有多余空格。
2. 检查 ~/.myclaw/myclaw.yaml 或环境变量。可以临时在终端 export 密钥再运行。
3. 前往对应提供商后台,确认密钥有效且额度充足,并确认所选模型在你的计划中可用。
启动 gateway 时提示端口被占用 端口3000已被其他程序(如另一个Node应用、开发服务器)使用。 1. 使用 lsof -i :3000 (macOS/Linux) 或 netstat -ano | findstr :3000 (Windows) 查找占用进程。
2. 终止占用进程,或修改 myclaw.yaml 中的 gateway.port 为其他端口(如 3001 )。
Telegram Bot 无响应 1. TELEGRAM_BOT_TOKEN 环境变量未设置或错误。
2. Bot未启动或网络问题。
3. 网关服务器无法被Telegram服务器访问(如果使用Webhook)。
1. 用 echo $TELEGRAM_BOT_TOKEN 确认环境变量。
2. 在Telegram中给Bot发送 /start ,检查Bot状态。
3. 如果使用Webhook :确保你的服务器有公网IP,且设置了正确的Webhook URL。 如果使用长轮询 :确保你的服务器能访问 api.telegram.org
飞书机器人验证失败或收不到事件 1. FEISHU_APP_ID FEISHU_APP_SECRET 错误。
2. 事件订阅URL未正确配置或验证未通过。
3. 应用未发布或机器人未添加到会话。
1. 在飞书开放平台后台核对应用凭证。
2. 最关键一步 :在“事件订阅”页面,确保请求URL正确,且飞书发送的 challenge 验证请求被你的服务器正确响应。查看网关日志是否有相关请求记录。
3. 将应用发布到“测试企业”,并确保在聊天中@了机器人。

6.2 运行时与功能问题

问题现象 可能原因 排查步骤与解决方案
AI回复慢或超时 1. LLM API响应慢(模型负载高)。
2. 网络连接问题。
3. 上下文过长,导致模型处理时间增加。
1. 尝试换一个模型(如从Claude-3.5 Sonnet换到Haiku)。
2. 检查网络延迟。对于海外API,考虑使用代理(需在配置中设置LLM API的 baseUrl 指向代理)。
3. 检查会话历史是否过长。在代码中实现上下文截断或摘要功能。
技能(Skill)未被触发 1. 技能未在配置文件中启用。
2. SKILL.md 文件格式错误,未能正确加载。
3. 技能提示词( ## prompt )描述不清,AI未能理解何时调用。
1. 检查 myclaw.yaml skills.enabled 列表。
2. 查看网关/agent启动日志,确认技能加载成功。检查 SKILL.md 的语法,特别是代码块标记是否正确。
3. 优化技能提示词,更明确地描述调用场景和参数。可以在对话中直接使用技能命令(如 /weather Beijing )测试。
流式响应在终端不显示或显示异常 1. 终端不支持ANSI转义码(某些旧终端或Windows CMD)。
2. 流式响应处理逻辑有误,chunk拼接出错。
1. 尝试在更现代的终端(如Windows Terminal, iTerm2, VS Code集成终端)中运行。
2. 在 src/agent/runtime.ts 的流式处理部分添加调试日志,打印每个收到的chunk,检查数据格式是否正确。
多轮对话后AI“失忆”或上下文混乱 会话历史未正确维护或传递。在内存存储模式下,重启服务会丢失历史。 1. 确认 AgentRuntime 在处理新消息时,正确地从“存储”中读取了历史消息并组合成上下文。
2. 长期方案 :实现一个持久化的会话存储层,将会话和消息保存到数据库中。

6.3 开发与调试问题

问题现象 可能原因 排查步骤与解决方案
TypeScript编译错误 1. 类型不匹配。
2. 缺少类型定义文件。
1. 仔细阅读错误信息,通常能定位到具体的文件和行号。检查函数参数和返回值类型。
2. 尝试安装缺失的 @types/ 包,例如 npm install --save-dev @types/ws
修改代码后重启无效 1. 使用的是编译后的JavaScript文件,而非TypeScript源码。
2. 进程未正确重启。
1. 确保使用 npm run dev 命令运行,它使用 tsx 直接执行 .ts 文件。
2. 如果使用 npx myclaw ,它是运行编译后的代码(位于 dist/ )。修改源码后需要重新运行 npm run build
无法导入本地模块 1. 文件路径错误。
2. 使用了错误的模块语法(CommonJS vs ESM)。
1. 检查 import 语句中的路径。MyClaw使用ES模块,路径应包含文件扩展名(如 import { Router } from ‘./router.js’; ),即使源文件是 .ts
2. 确保 package.json 中有 “type”: “module” 字段。

这个由2600行代码构成的MyClaw项目,就像一张精心绘制的地图,清晰地标出了构建一个现代AI助手框架所需的核心路径和关键地标。通过亲手实现它,你获得的不仅仅是一个可运行的玩具,而是一套可迁移的架构思维和工程实践。当你下次需要为你的产品添加一个AI聊天功能,或者想要构建一个自动化的流程助手时,你会知道从哪里开始设计,如何组织通道、路由和技能,以及如何避免那些我踩过的坑。真正的价值不在于复现这2600行,而在于理解其背后的设计决策,并能够以此为基础,去构建解决你自己独特问题的、成千上万行代码的系统。

更多推荐