Claude Code 从零复刻教程 第 2 篇:REPL 循环实现

目标:实现一个可交互的 REPL 循环,让用户能够持续输入指令并接收 AI 响应。


Claude Code 从零复刻教程 - 完整大纲
Claude Code 从零复刻教程 第 1 篇:项目初始化与 CLI 骨架
Claude Code 从零复刻教程 第 2 篇:REPL 循环实现


学习目标

1

完成本篇后,你将能够:

  1. 理解 REPL 循环的工作原理
  2. 使用 Node.js 的 readline 模块实现交互式输入
  3. 设计优雅的对话状态管理
  4. 实现退出命令和特殊指令处理
  5. 添加输入历史记录和快捷键支持

核心概念

什么是 REPL?

REPL 是 Read-Eval-Print Loop 的缩写,代表一种交互式编程环境:

复制代码
┌─────────────────────────────────────┐
│  Read  →  Eval  →  Print  →  Loop   │
│  (读取)   (执行)   (输出)   (循环)   │
└─────────────────────────────────────┘

工作流程:

  1. Read:读取用户输入
  2. Eval:执行/处理输入
  3. Print:输出结果
  4. Loop:回到第一步,等待下一次输入

Claude Code 的 REPL 特点

特性 说明
持续对话 保持上下文,支持多轮对话
特殊命令 /help, /exit, /clear
快捷键 Ctrl+C 退出,Ctrl+D EOF
历史记录 上下箭头浏览历史输入
多行输入 Shift+Enter 换行

代码实现

1. 项目结构

在上一篇的基础上,更新项目结构:

复制代码
my-claude/
├── src/
│   ├── index.ts          # 入口(保持不变)
│   ├── main.ts           # 主程序(更新)
│   └── repl.ts           # REPL 核心(新增)
├── package.json
└── tsconfig.json

2. REPL 核心模块

创建 src/repl.ts

typescript 复制代码
import * as readline from 'readline';
import { stdin as input, stdout as output } from 'process';
import pc from 'picocolors';

/**
 * REPL 配置选项
 */
export interface REPLConfig {
  /** 提示符 */
  prompt?: string;
  /** 欢迎信息 */
  welcomeMessage?: string;
  /** 是否显示时间戳 */
  showTimestamp?: boolean;
}

/**
 * REPL 回调函数类型
 */
export type REPLHandler = (input: string) => Promise<string | void>;

/**
 * REPL 会话状态
 */
interface SessionState {
  /** 输入历史 */
  history: string[];
  /** 当前历史索引 */
  historyIndex: number;
  /** 是否正在运行 */
  isRunning: boolean;
  /** 多行输入缓冲区 */
  multilineBuffer: string;
  /** 是否在多行模式 */
  isMultiline: boolean;
}

/**
 * 创建并启动 REPL 会话
 */
export function startREPL(handler: REPLHandler, config: REPLConfig = {}): void {
  const {
    prompt = '❯ ',
    welcomeMessage = 'Welcome to My Claude! Type /help for commands.',
    showTimestamp = false,
  } = config;

  // 初始化会话状态
  const state: SessionState = {
    history: [],
    historyIndex: -1,
    isRunning: true,
    multilineBuffer: '',
    isMultiline: false,
  };

  // 创建 readline 接口
  const rl = readline.createInterface({
    input,
    output,
    prompt: pc.cyan(prompt),
    historySize: 1000,
  });

  // 显示欢迎信息
  console.log(pc.green(welcomeMessage));
  console.log();

  // 显示初始提示符
  rl.prompt();

  // 处理输入行
  rl.on('line', async (line: string) => {
    const trimmedLine = line.trim();

    // 处理多行输入模式
    if (state.isMultiline) {
      if (trimmedLine === '```') {
        // 结束多行输入
        state.isMultiline = false;
        const fullInput = state.multilineBuffer;
        state.multilineBuffer = '';
        rl.setPrompt(pc.cyan(prompt));
        await processInput(fullInput, handler, rl, state, showTimestamp);
      } else {
        // 继续收集多行输入
        state.multilineBuffer += line + '\n';
        rl.setPrompt(pc.gray('... '));
        rl.prompt();
      }
      return;
    }

    // 处理空输入
    if (!trimmedLine) {
      rl.prompt();
      return;
    }

    // 处理特殊命令
    if (await handleCommand(trimmedLine, state, rl)) {
      return;
    }

    // 检查是否开始多行输入(以 ```开头)
    if (trimmedLine === '```') {
      state.isMultiline = true;
      state.multilineBuffer = '';
      rl.setPrompt(pc.gray('... '));
      rl.prompt();
      return;
    }

    // 处理普通输入
    await processInput(trimmedLine, handler, rl, state, showTimestamp);
  });

  // 处理 Ctrl+C (SIGINT)
  rl.on('SIGINT', () => {
    if (state.isMultiline) {
      // 取消多行输入
      state.isMultiline = false;
      state.multilineBuffer = '';
      console.log(pc.yellow('\n[多行输入已取消]'));
      rl.setPrompt(pc.cyan(prompt));
      rl.prompt();
    } else {
      gracefulExit(rl, state);
    }
  });

  // 处理 Ctrl+D (EOF)
  rl.on('close', () => {
    gracefulExit(rl, state);
  });
}

/**
 * 处理特殊命令
 * @returns 如果处理了命令返回 true
 */
async function handleCommand(
  input: string,
  state: SessionState,
  rl: readline.Interface
): Promise<boolean> {
  switch (input.toLowerCase()) {
    case '/exit':
    case '/quit':
    case 'exit':
    case 'quit':
      gracefulExit(rl, state);
      return true;

    case '/help':
      showHelp();
      rl.prompt();
      return true;

    case '/clear':
      console.clear();
      rl.prompt();
      return true;

    case '/history':
      showHistory(state.history);
      rl.prompt();
      return true;

    default:
      return false;
  }
}

/**
 * 显示帮助信息
 */
function showHelp(): void {
  console.log(pc.bold('\n📚 可用命令:\n'));
  console.log(`  ${pc.cyan('/help')}      显示此帮助信息`);
  console.log(`  ${pc.cyan('/exit')}      退出程序`);
  console.log(`  ${pc.cyan('/clear')}     清屏`);
  console.log(`  ${pc.cyan('/history')}   显示输入历史`);
  console.log(`  ${pc.cyan('```')}        开始/结束多行输入`);
  console.log();
  console.log(pc.gray('快捷键:'));
  console.log(`  ${pc.gray('Ctrl+C')}     退出(或取消多行输入)`);
  console.log(`  ${pc.gray('Ctrl+D')}     退出`);
  console.log(`  ${pc.gray('↑/↓')}        浏览历史输入`);
  console.log();
}

/**
 * 显示输入历史
 */
function showHistory(history: string[]): void {
  if (history.length === 0) {
    console.log(pc.yellow('暂无输入历史'));
    return;
  }

  console.log(pc.bold('\n📜 输入历史(最近 20 条):\n'));
  const recent = history.slice(-20);
  recent.forEach((item, index) => {
    const num = (history.length - recent.length + index + 1).toString().padStart(3);
    const truncated = item.length > 50 ? item.slice(0, 50) + '...' : item;
    console.log(`  ${pc.gray(num)}  ${truncated}`);
  });
  console.log();
}

/**
 * 处理用户输入
 */
async function processInput(
  input: string,
  handler: REPLHandler,
  rl: readline.Interface,
  state: SessionState,
  showTimestamp: boolean
): Promise<void> {
  // 记录到历史
  state.history.push(input);

  // 显示时间戳
  if (showTimestamp) {
    const timestamp = new Date().toLocaleTimeString();
    console.log(pc.gray(`[${timestamp}]`));
  }

  try {
    // 调用处理器
    const result = await handler(input);

    // 输出结果
    if (result !== undefined) {
      console.log(result);
    }
  } catch (error) {
    console.error(pc.red('错误:'), error instanceof Error ? error.message : String(error));
  }

  console.log(); // 空行分隔
  rl.prompt();
}

/**
 * 优雅退出
 */
function gracefulExit(rl: readline.Interface, state: SessionState): void {
  if (!state.isRunning) return;
  state.isRunning = false;

  console.log(pc.yellow('\n👋 再见!'));
  rl.close();
  process.exit(0);
}

3. 更新主程序

更新 src/main.ts

typescript 复制代码
import { program } from 'commander';
import pc from 'picocolors';
import { startREPL } from './repl';

// 模拟 AI 响应(第 4 篇将替换为真实 API 调用)
async function mockAIResponse(input: string): Promise<string> {
  // 模拟延迟
  await new Promise(resolve => setTimeout(resolve, 500));

  const responses = [
    `我收到了你的消息:"${input}"`,
    `这是一个模拟响应。在实际实现中,这里会调用 Claude API。`,
    `输入长度:${input.length} 个字符`,
  ];

  return responses.join('\n');
}

/**
 * 主程序入口
 */
export async function main(): Promise<void> {
  program
    .name('my-claude')
    .description('My Claude - 一个 Claude Code 复刻项目')
    .version('0.1.0')
    .option('-v, --verbose', '显示详细日志')
    .parse();

  const options = program.opts();

  if (options.verbose) {
    console.log(pc.gray('详细模式已开启'));
  }

  // 启动 REPL
  startREPL(
    async (input) => {
      // 这里处理用户输入
      if (options.verbose) {
        console.log(pc.gray(`[处理输入] ${input}`));
      }

      // 调用 AI(目前是模拟)
      return await mockAIResponse(input);
    },
    {
      prompt: '❯ ',
      welcomeMessage: '🤖 My Claude v0.1.0\n   基于 Claude Code 架构复刻',
      showTimestamp: options.verbose,
    }
  );
}

4. 更新 package.json

添加新的脚本:

json 复制代码
{
  "name": "my-claude",
  "version": "0.1.0",
  "description": "My Claude - 一个 Claude Code 复刻项目",
  "type": "module",
  "main": "dist/index.js",
  "bin": {
    "my-claude": "dist/index.js"
  },
  "scripts": {
    "build": "tsc",
    "dev": "tsx src/index.ts",
    "start": "node dist/index.js",
    "clean": "rm -rf dist"
  },
  "keywords": ["claude", "ai", "cli"],
  "author": "",
  "license": "MIT",
  "dependencies": {
    "commander": "^11.0.0",
    "picocolors": "^1.0.0"
  },
  "devDependencies": {
    "@types/node": "^20.0.0",
    "tsx": "^4.0.0",
    "typescript": "^5.0.0"
  }
}

运行演示

安装依赖

bash 复制代码
cd my-claude
npm install

运行程序

bash 复制代码
npm run dev

交互示例

复制代码
🤖 My Claude v0.1.0
   基于 Claude Code 架构复刻

❯ 你好
我收到了你的消息:"你好"
这是一个模拟响应。在实际实现中,这里会调用 Claude API。
输入长度:2 个字符

❯ /help

📚 可用命令:

  /help      显示此帮助信息
  /exit      退出程序
  /clear     清屏
  /history   显示输入历史
  ```开始/结束多行输入

快捷键:
  Ctrl+C     退出(或取消多行输入)
  Ctrl+D     退出
  ↑/↓        浏览历史输入

❯ ```
... 这是一段
... 多行输入
... ```
我收到了你的消息:"这是一段
多行输入
"
这是一个模拟响应。在实际实现中,这里会调用 Claude API。
输入长度:16 个字符

❯ /exit

👋 再见!

原理深入

Node.js readline 模块

readline 是 Node.js 内置模块,提供了逐行读取流的接口:

typescript 复制代码
import * as readline from 'readline';

const rl = readline.createInterface({
  input: process.stdin,   // 标准输入
  output: process.stdout, // 标准输出
  prompt: '> ',           // 提示符
  historySize: 1000,      // 历史记录大小
});

关键事件:

事件 触发时机
line 用户按下 Enter
SIGINT 用户按下 Ctrl+C
close 用户按下 Ctrl+D 或调用 rl.close()

多行输入实现

多行输入通过状态机实现:

复制代码
状态:NORMAL
  └── 输入 ```──→ 状态:MULTILINE
                      └── 输入 ```──→ 状态:NORMAL
                      └── 其他输入 ──→ 继续 MULTILINE

快捷键处理

快捷键 处理方式
Ctrl+C rl.on('SIGINT', ...)
Ctrl+D rl.on('close', ...)
↑/↓ readline 内置历史浏览
Tab readline 内置自动补全(可扩展)

练习作业

基础练习

  1. 添加新命令 :实现 /time 命令,显示当前时间

    答案

    typescript 复制代码
    // 在 repl.ts 的 handleCommand 函数中添加
    case '/time':
      const now = new Date();
      console.log(pc.cyan(`\n🕐 当前时间: ${now.toLocaleString()}\n`));
      rl.prompt();
      return true;
  2. 美化输出:为 AI 响应添加边框装饰

    答案

    typescript 复制代码
    // 在 processInput 函数中修改输出部分
    function formatOutput(text: string): string {
      const lines = text.split('\n');
      const maxLength = Math.max(...lines.map(l => l.length));
      const border = '─'.repeat(maxLength + 4);
      
      const formatted = lines
        .map(line => `│ ${line.padEnd(maxLength)} │`)
        .join('\n');
      
      return `┌${border}┐\n${formatted}\n└${border}┘`;
    }
    
    // 使用
    console.log(formatOutput(result));
  3. 输入计数:显示这是第几次对话

    答案

    typescript 复制代码
    // 在 SessionState 中添加计数器
    interface SessionState {
      history: string[];
      historyIndex: number;
      isRunning: boolean;
      multilineBuffer: string;
      isMultiline: boolean;
      inputCount: number;  // 新增
    }
    
    // 初始化时
    const state: SessionState = {
      // ... 其他字段
      inputCount: 0,
    };
    
    // 处理输入时递增
    async function processInput(...) {
      state.inputCount++;
      console.log(pc.gray(`[第 ${state.inputCount} 次对话]`));
      // ...
    }

进阶练习

  1. 自动补全:实现基于历史的 Tab 自动补全

    答案

    typescript 复制代码
    // 创建 readline 接口时添加 completer
    const rl = readline.createInterface({
      input,
      output,
      prompt: pc.cyan(prompt),
      historySize: 1000,
      completer: (line: string) => {
        const completions = state.history.filter(h => h.startsWith(line));
        const hits = completions.length ? completions : state.history;
        return [hits, line];
      },
    });
  2. 会话保存:将对话历史保存到文件,下次启动时加载

    答案

    typescript 复制代码
    import * as fs from 'fs/promises';
    import * as path from 'path';
    import * as os from 'os';
    
    const HISTORY_FILE = path.join(os.homedir(), '.myclaude', 'history.json');
    
    // 保存历史
    async function saveHistory(history: string[]): Promise<void> {
      await fs.mkdir(path.dirname(HISTORY_FILE), { recursive: true });
      await fs.writeFile(HISTORY_FILE, JSON.stringify(history), 'utf-8');
    }
    
    // 加载历史
    async function loadHistory(): Promise<string[]> {
      try {
        const content = await fs.readFile(HISTORY_FILE, 'utf-8');
        return JSON.parse(content);
      } catch {
        return [];
      }
    }
    
    // 在 startREPL 中初始化时加载
    state.history = await loadHistory();
    
    // 退出时保存
    function gracefulExit(rl: readline.Interface, state: SessionState): void {
      saveHistory(state.history);
      // ...
    }
  3. 语法高亮:对代码块进行简单的语法高亮

    答案

    typescript 复制代码
    function highlightCode(text: string): string {
      // 提取代码块
      const codeBlockRegex = /```(\w+)?\n([\s\S]*?)```/g;
      
      return text.replace(codeBlockRegex, (match, lang, code) => {
        // 简单的关键字高亮
        let highlighted = code
          .replace(/\b(function|const|let|var|return|if|else|for|while)\b/g, pc.cyan('$1'))
          .replace(/\b(true|false|null|undefined)\b/g, pc.yellow('$1'))
          .replace(/\/\/.*$/gm, pc.gray('$&'))
          .replace(/"[^"]*"/g, pc.green('$&'));
        
        return pc.gray('```') + (lang ? pc.yellow(lang) : '') + '\n' + highlighted + pc.gray('```');
      });
    }

挑战练习

  1. 多会话支持 :实现 /session new 创建新会话,/session list 列出所有会话

    答案

    typescript 复制代码
    // 会话管理
    interface Session {
      id: string;
      name: string;
      history: string[];
      createdAt: Date;
    }
    
    const sessions: Map<string, Session> = new Map();
    let currentSessionId: string = 'default';
    
    // 在 handleCommand 中添加
    case '/session':
      const subCmd = args[0];
      if (subCmd === 'new') {
        const newSession: Session = {
          id: Date.now().toString(),
          name: args[1] || `Session ${sessions.size + 1}`,
          history: [],
          createdAt: new Date(),
        };
        sessions.set(newSession.id, newSession);
        currentSessionId = newSession.id;
        console.log(pc.green(`✓ 创建新会话: ${newSession.name}`));
      } else if (subCmd === 'list') {
        console.log(pc.bold('\n📁 会话列表:\n'));
        sessions.forEach(s => {
          const marker = s.id === currentSessionId ? pc.green('●') : pc.gray('○');
          console.log(`  ${marker} ${s.name} (${s.history.length} 条消息)`);
        });
        console.log();
      }
      rl.prompt();
      return true;
  2. 实时打字效果:模拟 AI 逐字输出的打字机效果

    答案

    typescript 复制代码
    async function typewriterEffect(text: string, delay: number = 30): Promise<void> {
      for (const char of text) {
        process.stdout.write(char);
        await new Promise(resolve => setTimeout(resolve, delay));
      }
      console.log(); // 换行
    }
    
    // 在 processInput 中使用
    try {
      const result = await handler(input);
      if (result) {
        await typewriterEffect(result, 20);
      }
    } catch (error) {
      // ...
    }

下一篇预告

第 3 篇:参数解析与配置管理

我们将实现:

  • 环境变量管理(ANTHROPIC_API_KEY)
  • 配置文件系统(JSON/YAML)
  • 配置验证和默认值
  • 敏感信息加密存储

参考资料

相关推荐
刀法如飞5 小时前
Claude Code 命令速查与实践手册
aigc·ai编程·claude
量子位5 小时前
不只是卖服务器,中兴通讯想做AI时代的基础设施商
openai·ai编程
爱分享的阿Q6 小时前
AI编程工具Agent时代横评ClaudeCode-Cursor3-Copilot
copilot·ai编程
XPoet6 小时前
AI 编程工程化:Subagent——给你的 AI 员工打造协作助手
前端·后端·ai编程
byzh_rc6 小时前
[AI编程从入门到入土] 配置文件
java·数据库·ai编程
爱吃的小肥羊6 小时前
Claude Code 国内使用教程:手把手教你接入 Kimi 模型,零门槛开搞(2026 最新版)
aigc·ai编程
乐乐同学yorsal7 小时前
一个 TypeScript 写的图片视频处理工具箱,吊打一切付费软件!
前端·命令行
爱吃的小肥羊8 小时前
Claude 账号又被封了?亲测 3 种国内使用Claude Code 的靠谱方案!
aigc·ai编程
現実君8 小时前
现代化嵌入式AI编程-IDEA指南
java·intellij-idea·ai编程