Skip to content

Latest commit

 

History

History
712 lines (582 loc) · 18.4 KB

File metadata and controls

712 lines (582 loc) · 18.4 KB

Claude Code CLI 代码集成完整攻略

目录

  1. 概述
  2. 前置准备
  3. 核心实现
  4. 完整代码
  5. 使用示例
  6. 最佳实践
  7. 故障排除

概述

本攻略提供了在Node.js应用中直接调用Claude Code CLI的完整解决方案,基于对Claude Code Base Action的深度分析,提取了所有核心技术要点。

为什么需要直接调用

  • 灵活性: 在任何Node.js环境中使用,不限于GitHub Actions
  • 集成性: 可以整合到现有的应用程序和工作流中
  • 可控性: 完全控制执行参数、输出处理和错误处理
  • 扩展性: 可以根据需求自定义功能和输出格式

前置准备

1. 环境要求

按照Claude-Code-Cli

2. Node.js依赖

{
  "dependencies": {
    "@types/node": "^20.0.0"
  },
  "devDependencies": {
    "typescript": "^5.0.0"
  }
}

3. 系统要求

  • Unix/Linux/macOS (支持mkfifo命令)
  • Node.js 18+
  • 已安装jq命令行工具 (用于JSON处理)

核心实现

关键技术要点

  1. 工作目录管理: Claude必须在正确的项目目录下运行
  2. Named Pipes通信: 使用命名管道避免stdin/stdout复杂性
  3. 流式输出处理: 实时解析结构化JSON输出
  4. 进程生命周期管理: 超时控制和优雅终止
  5. 错误处理和资源清理: 确保系统稳定性

接口设计

export interface ClaudeExecutionOptions {
  prompt: string;                    // 执行的prompt
  model?: string;                    // 模型名称
  maxTurns?: number;                 // 最大对话轮数
  allowedTools?: string[];           // 允许的工具列表
  disallowedTools?: string[];        // 禁用的工具列表
  systemPrompt?: string;             // 系统提示词
  appendSystemPrompt?: string;       // 追加系统提示词
  mcpConfig?: string;                // MCP配置路径
  timeoutMinutes?: number;           // 超时时间(分钟)
  outputFile?: string;               // 输出文件路径
  workingDirectory?: string;         // 工作目录(关键!)
}

export interface ClaudeExecutionResult {
  success: boolean;                  // 执行是否成功
  exitCode: number;                  // 退出代码
  output: string;                    // 原始输出
  executionLog?: any[];              // 结构化执行日志
  outputFile?: string;               // 输出文件路径
}

完整代码

import { spawn, exec } from 'child_process';
import { promisify } from 'util';
import { writeFile, unlink, mkdir } from 'fs/promises';
import { createWriteStream } from 'fs';
import path from 'path';

const execAsync = promisify(exec);

export class ClaudeCodeRunner {
  private tempDir: string;
  
  constructor(tempDir: string = '/tmp/claude-runner') {
    this.tempDir = tempDir;
  }

  /**
   * 执行Claude Code命令
   */
  async runClaude(options: ClaudeExecutionOptions): Promise<ClaudeExecutionResult> {
    // 保存原始工作目录
    const originalCwd = process.cwd();
    
    try {
      // 1. 切换到指定工作目录(关键步骤!)
      if (options.workingDirectory) {
        console.log(`🔄 Changing to working directory: ${options.workingDirectory}`);
        process.chdir(options.workingDirectory);
      }
      
      // 2. 确保临时目录存在
      await mkdir(this.tempDir, { recursive: true });
      
      const promptPath = path.join(this.tempDir, `prompt-${Date.now()}.txt`);
      const pipePath = path.join(this.tempDir, `claude_pipe-${Date.now()}`);
      const outputFile = options.outputFile || 
        path.join(this.tempDir, `claude-output-${Date.now()}.json`);
      
      try {
        // 3. 准备prompt文件
        await writeFile(promptPath, options.prompt);
        console.log(`📝 Prompt written to: ${promptPath}`);
        
        // 4. 创建命名管道
        await this.createNamedPipe(pipePath);
        console.log(`🔗 Named pipe created: ${pipePath}`);
        
        // 5. 构建Claude命令参数
        const claudeArgs = this.buildClaudeArgs(options);
        console.log(`⚙️  Claude args: ${claudeArgs.join(' ')}`);
        
        // 6. 执行Claude进程
        const result = await this.executeClaudeProcess(
          claudeArgs,
          promptPath,
          pipePath,
          outputFile,
          options
        );
        
        return result;
      } finally {
        // 7. 清理临时文件
        await this.cleanup([promptPath, pipePath]);
      }
    } finally {
      // 8. 恢复原始工作目录
      process.chdir(originalCwd);
    }
  }

  /**
   * 创建命名管道
   */
  private async createNamedPipe(pipePath: string): Promise<void> {
    try {
      await unlink(pipePath);
    } catch (e) {
      // 忽略文件不存在的错误
    }
    await execAsync(`mkfifo "${pipePath}"`);
  }

  /**
   * 构建Claude命令行参数
   */
  private buildClaudeArgs(options: ClaudeExecutionOptions): string[] {
    const args = ['-p', '--verbose', '--output-format', 'stream-json'];
    
    if (options.model) {
      args.push('--model', options.model);
    }
    if (options.maxTurns) {
      args.push('--max-turns', options.maxTurns.toString());
    }
    if (options.allowedTools?.length) {
      args.push('--allowedTools', options.allowedTools.join(','));
    }
    if (options.disallowedTools?.length) {
      args.push('--disallowedTools', options.disallowedTools.join(','));
    }
    if (options.systemPrompt) {
      args.push('--system-prompt', options.systemPrompt);
    }
    if (options.appendSystemPrompt) {
      args.push('--append-system-prompt', options.appendSystemPrompt);
    }
    if (options.mcpConfig) {
      args.push('--mcp-config', options.mcpConfig);
    }
    
    return args;
  }

  /**
   * 执行Claude进程
   */
  private async executeClaudeProcess(
    claudeArgs: string[],
    promptPath: string,
    pipePath: string,
    outputFile: string,
    options: ClaudeExecutionOptions
  ): Promise<ClaudeExecutionResult> {
    return new Promise((resolve) => {
      let output = '';
      let resolved = false;
      
      console.log(`🚀 Starting Claude process...`);
      
      // 启动Claude进程 - 继承当前工作目录
      const claudeProcess = spawn('claude', claudeArgs, {
        stdio: ['pipe', 'pipe', 'inherit'],
        // 不需要显式设置cwd,会继承process.cwd()
        env: {
          ...process.env,
          // 可以在这里添加其他环境变量
        }
      });

      // 设置超时
      const timeoutMs = (options.timeoutMinutes || 10) * 60 * 1000;
      const timeoutId = setTimeout(() => {
        if (!resolved) {
          console.error(`⏰ Claude process timed out after ${timeoutMs / 1000} seconds`);
          claudeProcess.kill('SIGTERM');
          setTimeout(() => claudeProcess.kill('SIGKILL'), 5000);
          resolved = true;
          resolve({
            success: false,
            exitCode: 124,
            output,
            outputFile
          });
        }
      }, timeoutMs);

      // 处理输出
      claudeProcess.stdout.on('data', (data) => {
        const text = data.toString();
        output += text;
        
        // 实时输出处理
        this.processStreamOutput(text);
      });

      // 处理进程退出
      claudeProcess.on('close', async (code) => {
        if (!resolved) {
          clearTimeout(timeoutId);
          resolved = true;
          
          console.log(`✅ Claude process completed with exit code: ${code}`);
          
          // 处理输出并保存到文件
          const executionLog = await this.processOutputToJson(output, outputFile);
          
          resolve({
            success: code === 0,
            exitCode: code || 0,
            output,
            executionLog,
            outputFile
          });
        }
      });

      claudeProcess.on('error', (error) => {
        if (!resolved) {
          console.error('❌ Claude process error:', error);
          clearTimeout(timeoutId);
          resolved = true;
          resolve({
            success: false,
            exitCode: 1,
            output,
            outputFile
          });
        }
      });

      // 通过命名管道发送prompt
      this.sendPromptThroughPipe(promptPath, pipePath, claudeProcess);
    });
  }

  /**
   * 处理流式输出
   */
  private processStreamOutput(text: string): void {
    const lines = text.split('\n');
    lines.forEach((line) => {
      if (line.trim() === '') return;
      
      try {
        const parsed = JSON.parse(line);
        console.log('📊', JSON.stringify(parsed, null, 2));
      } catch (e) {
        console.log('📝', line);
      }
    });
  }

  /**
   * 通过命名管道发送prompt
   */
  private async sendPromptThroughPipe(
    promptPath: string, 
    pipePath: string, 
    claudeProcess: any
  ): Promise<void> {
    // 启动cat进程发送prompt到管道
    const catProcess = spawn('cat', [promptPath], {
      stdio: ['ignore', 'pipe', 'inherit']
    });
    
    const pipeStream = createWriteStream(pipePath);
    catProcess.stdout.pipe(pipeStream);

    // 从管道读取并发送到Claude
    const pipeProcess = spawn('cat', [pipePath]);
    pipeProcess.stdout.pipe(claudeProcess.stdin);

    // 错误处理
    catProcess.on('error', (error) => {
      console.error('❌ Cat process error:', error);
      pipeStream.destroy();
    });

    pipeProcess.on('error', (error) => {
      console.error('❌ Pipe process error:', error);
      claudeProcess.kill('SIGTERM');
    });
  }

  /**
   * 处理输出为JSON格式
   */
  private async processOutputToJson(output: string, outputFile: string): Promise<any[]> {
    try {
      // 将输出保存到临时文件
      const tempFile = outputFile.replace('.json', '.txt');
      await writeFile(tempFile, output);
      
      // 使用jq处理为JSON数组
      const { stdout: jsonOutput } = await execAsync(`jq -s '.' "${tempFile}"`);
      await writeFile(outputFile, jsonOutput);
      
      console.log(`💾 Execution log saved to: ${outputFile}`);
      return JSON.parse(jsonOutput);
    } catch (e) {
      console.warn(`⚠️  Failed to process output to JSON: ${e}`);
      return [];
    }
  }

  /**
   * 清理临时文件
   */
  private async cleanup(paths: string[]): Promise<void> {
    for (const path of paths) {
      try {
        await unlink(path);
      } catch (e) {
        // 忽略清理错误
      }
    }
    console.log('🧹 Temporary files cleaned up');
  }

  /**
   * 便捷方法:将结果输出到Markdown文件
   */
  async runAndSaveToMarkdown(
    options: ClaudeExecutionOptions,
    markdownPath: string
  ): Promise<ClaudeExecutionResult> {
    const result = await this.runClaude(options);
    
    // 创建Markdown内容
    const markdownContent = this.formatResultAsMarkdown(result, options);
    await writeFile(markdownPath, markdownContent);
    
    console.log(`📄 Results saved to Markdown: ${markdownPath}`);
    return result;
  }

  /**
   * 格式化结果为Markdown
   */
  private formatResultAsMarkdown(
    result: ClaudeExecutionResult,
    options: ClaudeExecutionOptions
  ): string {
    const timestamp = new Date().toISOString();
    
    return `# Claude Code Execution Result

## Execution Info
- **Timestamp**: ${timestamp}
- **Success**: ${result.success ? '✅' : '❌'}
- **Exit Code**: ${result.exitCode}
- **Model**: ${options.model || 'default'}
- **Max Turns**: ${options.maxTurns || 'unlimited'}
- **Working Directory**: ${options.workingDirectory || process.cwd()}

## Prompt
\`\`\`
${options.prompt}
\`\`\`

## Output
\`\`\`
${result.output}
\`\`\`

## Execution Log
${result.executionLog ? '```json\n' + JSON.stringify(result.executionLog, null, 2) + '\n```' : 'No execution log available'}

---
*Generated by Claude Code Runner at ${timestamp}*
`;
  }
}

使用示例

1. 基本使用

import { ClaudeCodeRunner } from './claude-code-runner';

async function basicExample() {
  const runner = new ClaudeCodeRunner();
  
  try {
    const result = await runner.runClaude({
      prompt: "分析当前项目的结构,生成一个项目概述",
      model: "claude-3-5-sonnet-20241022",
      maxTurns: 5,
      allowedTools: ["Read", "Glob", "Grep", "Bash"],
      timeoutMinutes: 15,
      workingDirectory: process.cwd() // 明确指定项目目录
    });
    
    console.log('✅ Execution completed:', result.success);
    if (result.success) {
      console.log('📊 Output preview:', result.output.substring(0, 500) + '...');
    } else {
      console.error('❌ Execution failed with exit code:', result.exitCode);
    }
  } catch (error) {
    console.error('❌ Error:', error);
  }
}

2. 保存到Markdown文件

async function markdownExample() {
  const runner = new ClaudeCodeRunner();
  
  try {
    const result = await runner.runAndSaveToMarkdown({
      prompt: "帮我优化这个函数的性能,并解释优化原理",
      model: "claude-3-5-sonnet-20241022",
      systemPrompt: "你是一个性能优化专家,专注于提供详细的代码分析和优化建议。",
      maxTurns: 3,
      allowedTools: ["Read", "Edit", "Grep"],
      timeoutMinutes: 10,
      workingDirectory: "/path/to/your/project"
    }, './claude-optimization-result.md');
    
    console.log('📄 Results saved to: ./claude-optimization-result.md');
  } catch (error) {
    console.error('❌ Error:', error);
  }
}

3. 批量处理

async function batchProcessing() {
  const runner = new ClaudeCodeRunner();
  const tasks = [
    {
      name: "代码审查",
      prompt: "审查src目录下的所有TypeScript文件,找出潜在问题",
      outputFile: "./reviews/code-review.md"
    },
    {
      name: "文档生成",
      prompt: "为主要的API函数生成文档",
      outputFile: "./docs/api-docs.md"
    },
    {
      name: "测试建议",
      prompt: "分析测试覆盖率,提供测试改进建议",
      outputFile: "./reports/test-suggestions.md"
    }
  ];

  for (const task of tasks) {
    console.log(`🔄 Processing: ${task.name}`);
    
    try {
      const result = await runner.runAndSaveToMarkdown({
        prompt: task.prompt,
        model: "claude-3-5-sonnet-20241022",
        maxTurns: 5,
        allowedTools: ["Read", "Glob", "Grep"],
        timeoutMinutes: 20,
        workingDirectory: process.cwd()
      }, task.outputFile);
      
      console.log(`✅ ${task.name} completed: ${result.success}`);
    } catch (error) {
      console.error(`❌ ${task.name} failed:`, error);
    }
  }
}

最佳实践

1. 工作目录管理

// ✅ 推荐:明确指定工作目录
const result = await runner.runClaude({
  workingDirectory: "/absolute/path/to/project",
  // ... 其他选项
});

// ❌ 避免:依赖当前目录
const result = await runner.runClaude({
  // workingDirectory 未指定
});

2. 工具权限控制

// ✅ 推荐:明确指定允许的工具
const result = await runner.runClaude({
  allowedTools: ["Read", "Glob", "Grep"], // 只读操作
  prompt: "分析代码结构"
});

// ⚠️  谨慎:允许写入操作
const result = await runner.runClaude({
  allowedTools: ["Read", "Write", "Edit", "Bash"], // 包含写入操作
  prompt: "优化代码"
});

3. 超时和资源管理

// ✅ 推荐:设置合理的超时时间
const result = await runner.runClaude({
  timeoutMinutes: 15, // 根据任务复杂度调整
  maxTurns: 5,        // 限制对话轮数
});

4. 错误处理

async function robustExecution() {
  const runner = new ClaudeCodeRunner();
  
  try {
    const result = await runner.runClaude(options);
    
    if (!result.success) {
      console.error(`Claude execution failed with exit code: ${result.exitCode}`);
      console.error(`Output: ${result.output}`);
      return;
    }
    
    // 处理成功结果
    console.log('Success!', result.output);
    
  } catch (error) {
    if (error.code === 'ENOENT') {
      console.error('❌ Claude CLI not found. Install with: npm install -g @anthropic-ai/claude-code');
    } else if (error.code === 'EACCES') {
      console.error('❌ Permission denied. Check file permissions.');
    } else {
      console.error('❌ Unexpected error:', error);
    }
  }
}

故障排除

常见问题及解决方案

1. Claude CLI未找到

Error: spawn claude ENOENT

解决方案:

npm install -g @anthropic-ai/claude-code@latest
which claude  # 验证安装路径

2. 权限问题

Error: EACCES: permission denied

解决方案:

# 检查临时目录权限
ls -la /tmp/claude-runner/
chmod 755 /tmp/claude-runner/

# 或使用不同的临时目录
const runner = new ClaudeCodeRunner('./temp/claude');

3. Named Pipe创建失败

Error: mkfifo command not found

解决方案:

  • 确保在Unix/Linux/macOS系统上运行
  • Windows用户需要使用WSL或Docker

4. jq命令未找到

Error: jq: command not found

解决方案:

# macOS
brew install jq

# Ubuntu/Debian
sudo apt-get install jq

# CentOS/RHEL
sudo yum install jq

调试技巧

1. 启用详细日志

// 在环境变量中启用调试
process.env.DEBUG = 'claude-runner:*';

2. 保留临时文件

class DebugClaudeCodeRunner extends ClaudeCodeRunner {
  private async cleanup(paths: string[]): Promise<void> {
    console.log('🐛 Debug mode: Skipping cleanup');
    console.log('📁 Temp files:', paths);
    // 不删除临时文件,便于调试
  }
}

3. 输出分析

const result = await runner.runClaude(options);

// 分析原始输出
console.log('Raw output length:', result.output.length);
console.log('Exit code:', result.exitCode);
console.log('Execution log entries:', result.executionLog?.length || 0);

// 保存调试信息
await writeFile('./debug-output.txt', result.output);

总结

这个完整攻略提供了在Node.js中直接调用Claude Code CLI的所有必要信息:

  1. 核心实现: 基于Named Pipes的稳定进程间通信
  2. 工作目录管理: 确保Claude在正确的项目上下文中运行
  3. 完整的错误处理: 超时控制、资源清理和异常处理
  4. 灵活的输出处理: 支持实时输出和结构化日志
  5. 便捷的集成接口: 易于集成到现有应用中

通过遵循这个攻略,你可以在任何Node.js项目中稳定地使用Claude Code的强大功能。